From d604f9adfb265b9311c2b66a2027256e9cd8b064 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 13 Feb 2019 18:18:47 -0800 Subject: [PATCH 001/117] Add AudioSoloRequest, BulkAvatarTraitsAck; also decode obfuscated protocols --- tools/dissectors/1-hfudt.lua | 98 ++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/tools/dissectors/1-hfudt.lua b/tools/dissectors/1-hfudt.lua index de99c1ce3c..8179276dbb 100644 --- a/tools/dissectors/1-hfudt.lua +++ b/tools/dissectors/1-hfudt.lua @@ -152,7 +152,9 @@ local packet_types = { [97] = "OctreeDataPersist", [98] = "EntityClone", [99] = "EntityQueryInitialResultsComplete", - [100] = "BulkAvatarTraits" + [100] = "BulkAvatarTraits", + [101] = "AudioSoloRequest", + [102] = "BulkAvatarTraitsAck" } local unsourced_packet_types = { @@ -301,55 +303,53 @@ function p_hfudt.dissector(buf, pinfo, tree) -- check if we have part of a message that we need to re-assemble -- before it can be dissected - if obfuscation_bits == 0 then - if message_bit == 1 and message_position ~= 0 then - if fragments[message_number] == nil then - fragments[message_number] = {} - end - - if fragments[message_number][message_part_number] == nil then - fragments[message_number][message_part_number] = {} - end - - -- set the properties for this fragment - fragments[message_number][message_part_number] = { - payload = buf(i):bytes() - } - - -- if this is the last part, set our maximum part number - if message_position == 1 then - fragments[message_number].last_part_number = message_part_number - end - - -- if we have the last part - -- enumerate our parts for this message and see if everything is present - if fragments[message_number].last_part_number ~= nil then - local i = 0 - local has_all = true - - local finalMessage = ByteArray.new() - local message_complete = true - - while i <= fragments[message_number].last_part_number do - if fragments[message_number][i] ~= nil then - finalMessage = finalMessage .. fragments[message_number][i].payload - else - -- missing this part, have to break until we have it - message_complete = false - end - - i = i + 1 - end - - if message_complete then - debug("Message " .. message_number .. " is " .. finalMessage:len()) - payload_to_dissect = ByteArray.tvb(finalMessage, message_number) - end - end - - else - payload_to_dissect = buf(i):tvb() + if message_bit == 1 and message_position ~= 0 then + if fragments[message_number] == nil then + fragments[message_number] = {} end + + if fragments[message_number][message_part_number] == nil then + fragments[message_number][message_part_number] = {} + end + + -- set the properties for this fragment + fragments[message_number][message_part_number] = { + payload = buf(i):bytes() + } + + -- if this is the last part, set our maximum part number + if message_position == 1 then + fragments[message_number].last_part_number = message_part_number + end + + -- if we have the last part + -- enumerate our parts for this message and see if everything is present + if fragments[message_number].last_part_number ~= nil then + local i = 0 + local has_all = true + + local finalMessage = ByteArray.new() + local message_complete = true + + while i <= fragments[message_number].last_part_number do + if fragments[message_number][i] ~= nil then + finalMessage = finalMessage .. fragments[message_number][i].payload + else + -- missing this part, have to break until we have it + message_complete = false + end + + i = i + 1 + end + + if message_complete then + debug("Message " .. message_number .. " is " .. finalMessage:len()) + payload_to_dissect = ByteArray.tvb(finalMessage, message_number) + end + end + + else + payload_to_dissect = buf(i):tvb() end if payload_to_dissect ~= nil then From e4d6d5af89674c1feb319944a49663689e2bdbbf Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Feb 2019 17:35:43 -0800 Subject: [PATCH 002/117] Streamline ModelBaker initialization and URLs --- libraries/baking/src/FBXBaker.cpp | 24 +---- libraries/baking/src/FBXBaker.h | 2 - libraries/baking/src/ModelBaker.cpp | 23 +++++ libraries/baking/src/ModelBaker.h | 4 + libraries/baking/src/OBJBaker.cpp | 13 +-- libraries/baking/src/baking/BakerLibrary.cpp | 76 +++++++++++++++ libraries/baking/src/baking/BakerLibrary.h | 28 ++++++ tools/oven/src/BakerCLI.cpp | 22 +++-- tools/oven/src/DomainBaker.cpp | 98 ++++++-------------- tools/oven/src/ui/ModelBakeWidget.cpp | 85 ++++++----------- 10 files changed, 201 insertions(+), 174 deletions(-) create mode 100644 libraries/baking/src/baking/BakerLibrary.cpp create mode 100644 libraries/baking/src/baking/BakerLibrary.h diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index afaca1dd62..7c4354a2b6 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -40,8 +40,8 @@ void FBXBaker::bake() { qDebug() << "FBXBaker" << _modelURL << "bake starting"; - // setup the output folder for the results of this bake - setupOutputFolder(); + // Setup the output folders for the results of this bake + initializeOutputDirs(); if (shouldStop()) { return; @@ -78,26 +78,6 @@ void FBXBaker::bakeSourceCopy() { checkIfTexturesFinished(); } -void FBXBaker::setupOutputFolder() { - // make sure there isn't already an output directory using the same name - if (QDir(_bakedOutputDir).exists()) { - qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; - } 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 " + _originalOutputDir); - return; - } - } -} - void FBXBaker::loadSourceFBX() { // check if the FBX is local or first needs to be downloaded if (_modelURL.isLocalFile()) { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 2af51b2190..88443de1c0 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -44,8 +44,6 @@ private slots: void handleFBXNetworkReply(); private: - void setupOutputFolder(); - void loadSourceFBX(); void importScene(); diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 34f302b501..6959a5c455 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -64,6 +64,29 @@ ModelBaker::~ModelBaker() { } } +void ModelBaker::initializeOutputDirs() { + // Attempt to make the output folders + // Warn if there is an output directory using the same name + + if (QDir(_bakedOutputDir).exists()) { + qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + } else { + qCDebug(model_baking) << "Creating baked output folder" << _bakedOutputDir; + if (!QDir().mkpath(_bakedOutputDir)) { + handleError("Failed to create baked output folder " + _bakedOutputDir); + } + } + + if (QDir(_originalOutputDir).exists()) { + qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; + } else { + qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir; + if (!QDir().mkpath(_originalOutputDir)) { + handleError("Failed to create original output folder " + _originalOutputDir); + } + } +} + void ModelBaker::abort() { Baker::abort(); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 14a182f622..dc9d43ad66 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -31,6 +31,8 @@ using TextureBakerThreadGetter = std::function; using GetMaterialIDCallback = std::function ; static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; +static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; +static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; class ModelBaker : public Baker { Q_OBJECT @@ -40,6 +42,8 @@ public: const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); virtual ~ModelBaker(); + void initializeOutputDirs(); + bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 5a1239f88f..11cac0b4c2 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -38,6 +38,9 @@ const QByteArray MESH = "Mesh"; void OBJBaker::bake() { qDebug() << "OBJBaker" << _modelURL << "bake starting"; + // Setup the output folders for the results of this bake + initializeOutputDirs(); + // trigger bakeOBJ once OBJ is loaded connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ); @@ -46,16 +49,6 @@ void OBJBaker::bake() { } void OBJBaker::loadOBJ() { - if (!QDir().mkpath(_bakedOutputDir)) { - handleError("Failed to create baked OBJ output folder " + _bakedOutputDir); - return; - } - - if (!QDir().mkpath(_originalOutputDir)) { - handleError("Failed to create original OBJ output folder " + _originalOutputDir); - return; - } - // check if the OBJ is local or it needs to be downloaded if (_modelURL.isLocalFile()) { // loading the local OBJ diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp new file mode 100644 index 0000000000..a587de97eb --- /dev/null +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -0,0 +1,76 @@ +// +// BakerLibrary.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/02/14. +// Copyright 2019 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 "BakerLibrary.h" + +#include "../FBXBaker.h" +#include "../OBJBaker.h" + +QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals) { + // Check if the file pointed to by this URL is a bakeable model, by comparing extensions + auto modelFileName = url.fileName(); + + bool isBakedModel = modelFileName.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive); + bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); + bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); + bool isBakeable = isBakeableFBX || isBakeableOBJ; + + if (isBakeable || (shouldRebakeOriginals && isBakedModel)) { + if (isBakedModel) { + // Grab a URL to the original, that we assume is stored a directory up, in the "original" folder + // with just the fbx extension + qDebug() << "Inferring original URL for baked model URL" << url; + + auto originalFileName = modelFileName; + originalFileName.replace(".baked", ""); + qDebug() << "Original model URL must be present at" << url; + + return url.resolved("../original/" + originalFileName); + } else { + // Grab a clean version of the URL without a query or fragment + return url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + } + } + + qWarning() << "Unknown model type: " << modelFileName; + return QUrl(); +} + +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) { + auto filename = bakeableModelURL.fileName(); + + // Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique + auto baseName = filename.left(filename.lastIndexOf('.')); + auto subDirName = "/" + baseName; + int i = 1; + while (QDir(contentOutputPath + subDirName).exists()) { + subDirName = "/" + baseName + "-" + QString::number(i++); + } + + QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked"; + QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; + + std::unique_ptr baker; + + if (filename.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + } else if (filename.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + } else { + qDebug() << "Could not create ModelBaker for url" << bakeableModelURL; + } + + if (baker) { + QDir(contentOutputPath).mkpath(subDirName); + } + + return baker; +} \ No newline at end of file diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h new file mode 100644 index 0000000000..8739b4e947 --- /dev/null +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -0,0 +1,28 @@ +// +// ModelBaker.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/02/14. +// Copyright 2019 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_BakerLibrary_h +#define hifi_BakerLibrary_h + +#include + +#include "../ModelBaker.h" + +// Returns either the given model URL, or, if the model is baked and shouldRebakeOriginals is true, +// the guessed location of the original model +// Returns an empty URL if no bakeable URL found +QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals); + +// Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored +// Returns an empty pointer if a baker could not be created +std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); + +#endif hifi_BakerLibrary_h diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index ff672d13bf..f4e64c3015 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -20,7 +20,7 @@ #include "OvenCLIApplication.h" #include "ModelBakingLoggingCategory.h" -#include "FBXBaker.h" +#include "baking/BakerLibrary.h" #include "JSBaker.h" #include "TextureBaker.h" @@ -41,7 +41,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& static const QString SCRIPT_EXTENSION { "js" }; // check what kind of baker we should be creating - bool isFBX = type == MODEL_EXTENSION; + bool isModel = type == MODEL_EXTENSION; bool isScript = type == SCRIPT_EXTENSION; // If the type doesn't match the above, we assume we have a texture, and the type specified is the @@ -54,13 +54,17 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _outputPath = outputPath; // create our appropiate baker - if (isFBX) { - _baker = std::unique_ptr { - new FBXBaker(inputUrl, - []() -> QThread* { return Oven::instance().getNextWorkerThread(); }, - outputPath) - }; - _baker->moveToThread(Oven::instance().getNextWorkerThread()); + if (isModel) { + QUrl bakeableModelURL = getBakeableModelURL(inputUrl, false); + if (!bakeableModelURL.isEmpty()) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + _baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputPath); + if (_baker) { + _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } + } } else if (isScript) { _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 0a75c72f9a..11a38f2f24 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -20,8 +20,7 @@ #include "Gzip.h" #include "Oven.h" -#include "FBXBaker.h" -#include "OBJBaker.h" +#include "baking/BakerLibrary.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, const QString& baseOutputPath, const QUrl& destinationPath, @@ -163,82 +162,37 @@ void DomainBaker::enumerateEntities() { // 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() }; + QUrl bakeableModelURL = getBakeableModelURL(entity[ENTITY_MODEL_URL_KEY].toString(), _shouldRebakeOriginals); - // check if the file pointed to by this URL is a bakeable model, by comparing extensions - auto modelFileName = modelURL.fileName(); - - static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; - static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; - static const QString BAKED_MODEL_EXTENSION = ".baked.fbx"; - - bool isBakedModel = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive); - bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); - bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); - bool isBakeable = isBakeableFBX || isBakeableOBJ; - - if (isBakeable || (_shouldRebakeOriginals && isBakedModel)) { - - if (isBakedModel) { - // grab a URL to the original, that we assume is stored a directory up, in the "original" folder - // with just the fbx extension - qDebug() << "Re-baking original for" << modelURL; - - auto originalFileName = modelFileName; - originalFileName.replace(".baked", ""); - modelURL = modelURL.resolved("../original/" + originalFileName); - - qDebug() << "Original must be present at" << modelURL; - } else { - // grab a clean version of the URL without a query or fragment - modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - } + if (!bakeableModelURL.isEmpty()) { // setup a ModelBaker for this URL, as long as we don't already have one - if (!_modelBakers.contains(modelURL)) { - auto filename = modelURL.fileName(); - auto baseName = filename.left(filename.lastIndexOf('.')); - auto subDirName = "/" + baseName; - int i = 1; - while (QDir(_contentOutputPath + subDirName).exists()) { - subDirName = "/" + baseName + "-" + QString::number(i++); + if (!_modelBakers.contains(bakeableModelURL)) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); + if (baker) { + // 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(bakeableModelURL, baker); + + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(Oven::instance().getNextWorkerThread()); + 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(bakeableModelURL, *it); } - QSharedPointer baker; - if (isBakeableFBX) { - baker = { - new FBXBaker(modelURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), - &FBXBaker::deleteLater - }; - } else { - baker = { - new OBJBaker(modelURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), - &OBJBaker::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(Oven::instance().getNextWorkerThread()); - 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. diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 9fa586871e..5ac9b43348 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -26,8 +26,7 @@ #include "../Oven.h" #include "../OvenGUIApplication.h" #include "OvenMainWindow.h" -#include "FBXBaker.h" -#include "OBJBaker.h" +#include "baking/BakerLibrary.h" static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory"; @@ -172,74 +171,42 @@ void ModelBakeWidget::bakeButtonClicked() { // 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") { - qDebug() << modelToBakeURL.toString(); - qDebug() << modelToBakeURL.scheme(); - modelToBakeURL = QUrl::fromLocalFile(fileURLString); - qDebug() << "New url: " << modelToBakeURL; - } - - auto modelName = modelToBakeURL.fileName().left(modelToBakeURL.fileName().lastIndexOf('.')); - // make sure we have a valid output directory QDir outputDirectory(_outputDirLineEdit->text()); - QString subFolderName = modelName + "/"; - - // output in a sub-folder with the name of the fbx, potentially suffixed by a number to make it unique - int iteration = 0; - - while (outputDirectory.exists(subFolderName)) { - subFolderName = modelName + "-" + QString::number(++iteration) + "/"; - } - - outputDirectory.mkpath(subFolderName); - if (!outputDirectory.exists()) { QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); return; } - outputDirectory.cd(subFolderName); + QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL), false); - QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked"); - QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original"); + if (!bakeableModelURL.isEmpty()) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; - bakedOutputDirectory.mkdir("."); - originalOutputDirectory.mkdir("."); + std::unique_ptr baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputDirectory.path()); + if (baker) { + // everything seems to be in place, kick off a bake for this model now - std::unique_ptr baker; - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - // everything seems to be in place, kick off a bake for this model now - if (modelToBakeURL.fileName().endsWith(".fbx")) { - baker.reset(new FBXBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), - originalOutputDirectory.absolutePath())); - } else if (modelToBakeURL.fileName().endsWith(".obj")) { - baker.reset(new OBJBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), - originalOutputDirectory.absolutePath())); - } else { - qWarning() << "Unknown model type: " << modelToBakeURL.fileName(); - continue; + // move the baker to the FBX baker thread + baker->moveToThread(Oven::instance().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(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker); + + // add a pending row to the results window to show that this bake is in process + auto resultsWindow = OvenGUIApplication::instance()->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); + } } - - // move the baker to the FBX baker thread - baker->moveToThread(Oven::instance().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(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker); - - // add a pending row to the results window to show that this bake is in process - auto resultsWindow = OvenGUIApplication::instance()->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); } } From 4ae0c79130341c89b7cb2dcef763e805a8db4876 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 20 Feb 2019 10:29:27 -0800 Subject: [PATCH 003/117] Harden model-baker Engine against random tasks being disabled --- libraries/model-baker/src/model-baker/Baker.cpp | 17 +++++++++-------- .../src/model-baker/BuildGraphicsMeshTask.cpp | 3 ++- .../CalculateBlendshapeTangentsTask.cpp | 4 ++-- .../model-baker/CalculateMeshTangentsTask.cpp | 2 +- .../model-baker/src/model-baker/ModelMath.h | 9 +++++++++ 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index dfb18eef86..344af1ba8a 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -14,6 +14,7 @@ #include #include "BakerTypes.h" +#include "ModelMath.h" #include "BuildGraphicsMeshTask.h" #include "CalculateMeshNormalsTask.h" #include "CalculateMeshTangentsTask.h" @@ -59,12 +60,12 @@ namespace baker { blendshapesPerMeshOut = blendshapesPerMeshIn; for (int i = 0; i < (int)blendshapesPerMeshOut.size(); i++) { - const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i]; - const auto& tangentsPerBlendshape = tangentsPerBlendshapePerMesh[i]; + const auto& normalsPerBlendshape = safeGet(normalsPerBlendshapePerMesh, i); + const auto& tangentsPerBlendshape = safeGet(tangentsPerBlendshapePerMesh, i); auto& blendshapesOut = blendshapesPerMeshOut[i]; for (int j = 0; j < (int)blendshapesOut.size(); j++) { - const auto& normals = normalsPerBlendshape[j]; - const auto& tangents = tangentsPerBlendshape[j]; + const auto& normals = safeGet(normalsPerBlendshape, j); + const auto& tangents = safeGet(tangentsPerBlendshape, j); auto& blendshape = blendshapesOut[j]; blendshape.normals = QVector::fromStdVector(normals); blendshape.tangents = QVector::fromStdVector(tangents); @@ -90,10 +91,10 @@ namespace baker { auto meshesOut = meshesIn; for (int i = 0; i < numMeshes; i++) { auto& meshOut = meshesOut[i]; - meshOut._mesh = graphicsMeshesIn[i]; - meshOut.normals = QVector::fromStdVector(normalsPerMeshIn[i]); - meshOut.tangents = QVector::fromStdVector(tangentsPerMeshIn[i]); - meshOut.blendshapes = QVector::fromStdVector(blendshapesPerMeshIn[i]); + meshOut._mesh = safeGet(graphicsMeshesIn, i); + meshOut.normals = QVector::fromStdVector(safeGet(normalsPerMeshIn, i)); + meshOut.tangents = QVector::fromStdVector(safeGet(tangentsPerMeshIn, i)); + meshOut.blendshapes = QVector::fromStdVector(safeGet(blendshapesPerMeshIn, i)); } output = meshesOut; } diff --git a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp index 370add2c2e..f329dc18f5 100644 --- a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp @@ -15,6 +15,7 @@ #include #include "ModelBakerLogging.h" +#include "ModelMath.h" using vec2h = glm::tvec2; @@ -385,7 +386,7 @@ void BuildGraphicsMeshTask::run(const baker::BakeContextPointer& context, const auto& graphicsMesh = graphicsMeshes[i]; // Try to create the graphics::Mesh - buildGraphicsMesh(meshes[i], graphicsMesh, normalsPerMesh[i], tangentsPerMesh[i]); + buildGraphicsMesh(meshes[i], graphicsMesh, baker::safeGet(normalsPerMesh, i), baker::safeGet(tangentsPerMesh, i)); // Choose a name for the mesh if (graphicsMesh) { diff --git a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp index 04e05f0378..ba8fd94f09 100644 --- a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp @@ -24,7 +24,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte tangentsPerBlendshapePerMeshOut.reserve(normalsPerBlendshapePerMesh.size()); for (size_t i = 0; i < blendshapesPerMesh.size(); i++) { - const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i]; + const auto& normalsPerBlendshape = baker::safeGet(normalsPerBlendshapePerMesh, i); const auto& blendshapes = blendshapesPerMesh[i]; const auto& mesh = meshes[i]; tangentsPerBlendshapePerMeshOut.emplace_back(); @@ -43,7 +43,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte for (size_t j = 0; j < blendshapes.size(); j++) { const auto& blendshape = blendshapes[j]; const auto& tangentsIn = blendshape.tangents; - const auto& normals = normalsPerBlendshape[j]; + const auto& normals = baker::safeGet(normalsPerBlendshape, j); tangentsPerBlendshapeOut.emplace_back(); auto& tangentsOut = tangentsPerBlendshapeOut[tangentsPerBlendshapeOut.size()-1]; diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp index 6e12ec546d..d2144a0e30 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp @@ -34,7 +34,7 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co for (int i = 0; i < (int)meshes.size(); i++) { const auto& mesh = meshes[i]; const auto& tangentsIn = mesh.tangents; - const auto& normals = normalsPerMesh[i]; + const auto& normals = baker::safeGet(normalsPerMesh, i); tangentsPerMeshOut.emplace_back(); auto& tangentsOut = tangentsPerMeshOut[tangentsPerMeshOut.size()-1]; diff --git a/libraries/model-baker/src/model-baker/ModelMath.h b/libraries/model-baker/src/model-baker/ModelMath.h index 2a909e6eed..60ce3ad098 100644 --- a/libraries/model-baker/src/model-baker/ModelMath.h +++ b/libraries/model-baker/src/model-baker/ModelMath.h @@ -14,6 +14,15 @@ #include "BakerTypes.h" namespace baker { + template + T safeGet(const std::vector& data, size_t i) { + if (data.size() > i) { + return data[i]; + } else { + return T(); + } + } + // Returns a reference to the normal at the specified index, or nullptr if it cannot be accessed using NormalAccessor = std::function; From 9c9dc553a21cb80b052f1d0a1f5465ca2be2cc1c Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 20 Feb 2019 12:00:43 -0800 Subject: [PATCH 004/117] Fix binding to temporary when trying to safely get empty model-baker task data --- libraries/model-baker/src/model-baker/ModelMath.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libraries/model-baker/src/model-baker/ModelMath.h b/libraries/model-baker/src/model-baker/ModelMath.h index 60ce3ad098..38bb3e1b3d 100644 --- a/libraries/model-baker/src/model-baker/ModelMath.h +++ b/libraries/model-baker/src/model-baker/ModelMath.h @@ -15,11 +15,13 @@ namespace baker { template - T safeGet(const std::vector& data, size_t i) { + const T& safeGet(const std::vector& data, size_t i) { + static T t; + if (data.size() > i) { return data[i]; } else { - return T(); + return t; } } From aef696efe6c1a4df3a9af2ff19ee9d6161f4cb40 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 20 Feb 2019 14:28:44 -0800 Subject: [PATCH 005/117] Add passthrough config to PrepareJointsTask --- .../src/model-baker/PrepareJointsTask.cpp | 57 +++++++++++-------- .../src/model-baker/PrepareJointsTask.h | 15 ++++- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 3b1a57cb43..63d0408337 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -50,37 +50,46 @@ QMap getJointRotationOffsets(const QVariantHash& mapping) { return jointRotationOffsets; } +void PrepareJointsTask::configure(const Config& config) { + _passthrough = config.passthrough; +} + void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& jointsIn = input.get0(); - const auto& mapping = input.get1(); auto& jointsOut = output.edit0(); - auto& jointRotationOffsets = output.edit1(); - auto& jointIndices = output.edit2(); - // Get joint renames - auto jointNameMapping = getJointNameMapping(mapping); - // Apply joint metadata from FST file mappings - for (const auto& jointIn : jointsIn) { - jointsOut.push_back(jointIn); - auto& jointOut = jointsOut.back(); + if (_passthrough) { + jointsOut = jointsIn; + } else { + const auto& mapping = input.get1(); + auto& jointRotationOffsets = output.edit1(); + auto& jointIndices = output.edit2(); - auto jointNameMapKey = jointNameMapping.key(jointIn.name); - if (jointNameMapping.contains(jointNameMapKey)) { - jointOut.name = jointNameMapKey; + // Get joint renames + auto jointNameMapping = getJointNameMapping(mapping); + // Apply joint metadata from FST file mappings + for (const auto& jointIn : jointsIn) { + jointsOut.push_back(jointIn); + auto& jointOut = jointsOut.back(); + + auto jointNameMapKey = jointNameMapping.key(jointIn.name); + if (jointNameMapping.contains(jointNameMapKey)) { + jointOut.name = jointNameMapKey; + } + + jointIndices.insert(jointOut.name, (int)jointsOut.size()); } - jointIndices.insert(jointOut.name, (int)jointsOut.size()); - } - - // Get joint rotation offsets from FST file mappings - auto offsets = getJointRotationOffsets(mapping); - for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { - QString jointName = itr.key(); - int jointIndex = jointIndices.value(jointName) - 1; - if (jointIndex != -1) { - glm::quat rotationOffset = itr.value(); - jointRotationOffsets.insert(jointIndex, rotationOffset); - qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; + // Get joint rotation offsets from FST file mappings + auto offsets = getJointRotationOffsets(mapping); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + int jointIndex = jointIndices.value(jointName) - 1; + if (jointIndex != -1) { + glm::quat rotationOffset = itr.value(); + jointRotationOffsets.insert(jointIndex, rotationOffset); + qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; + } } } } diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index e12d8ffd2c..6185d2fdad 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -18,13 +18,26 @@ #include "Engine.h" +// The property "passthrough", when enabled, will let the input joints flow to the output unmodified, unlike the disabled property, which discards the data +class PrepareJointsTaskConfig : public baker::JobConfig { + Q_OBJECT + Q_PROPERTY(bool passthrough MEMBER passthrough) +public: + bool passthrough { false }; +}; + class PrepareJointsTask { public: + using Config = PrepareJointsTaskConfig; using Input = baker::VaryingSet2, QVariantHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; - using JobModel = baker::Job::ModelIO; + using JobModel = baker::Job::ModelIO; + void configure(const Config& config); void run(const baker::BakeContextPointer& context, const Input& input, Output& output); + +protected: + bool _passthrough { false }; }; #endif // hifi_PrepareJointsTask_h \ No newline at end of file From 8ff212ac95dadc0bfaa9b06181703df0aa5f423a Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 10:19:00 -0800 Subject: [PATCH 006/117] Move custom draco mesh attributes from FBX.h to HFM.h --- libraries/fbx/src/FBX.h | 5 ----- libraries/hfm/src/hfm/HFM.h | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 8ad419c7ec..48ce994ac8 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -26,11 +26,6 @@ static const QByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); static const quint32 FBX_VERSION_2015 = 7400; static const quint32 FBX_VERSION_2016 = 7500; -static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; -static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; -static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; -static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2; - static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0; static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1; diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 9f3de3302c..826c79e911 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -53,6 +53,11 @@ using ColorType = glm::vec3; const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; +static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; +static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; +static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; +static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2; + // High Fidelity Model namespace namespace hfm { From 2af17015d3e31e68d9fcef288384b8477f0d80d9 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 25 Feb 2019 12:04:26 -0800 Subject: [PATCH 007/117] Convert serializers and FBX.h to use HifiTypes.h --- libraries/fbx/src/FBX.h | 9 +- libraries/fbx/src/FBXSerializer.cpp | 150 +++++++++---------- libraries/fbx/src/FBXSerializer.h | 14 +- libraries/fbx/src/FBXSerializer_Material.cpp | 3 +- libraries/fbx/src/FBXSerializer_Mesh.cpp | 8 +- libraries/fbx/src/FBXSerializer_Node.cpp | 14 +- libraries/fbx/src/GLTFSerializer.cpp | 46 +++--- libraries/fbx/src/GLTFSerializer.h | 26 ++-- libraries/fbx/src/OBJSerializer.cpp | 56 +++---- libraries/fbx/src/OBJSerializer.h | 30 ++-- 10 files changed, 177 insertions(+), 179 deletions(-) diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 48ce994ac8..342d605337 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -13,16 +13,17 @@ #define hifi_FBX_h_ #include -#include #include #include #include +#include + // See comment in FBXSerializer::parseFBX(). static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23; -static const QByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary "); -static const QByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); +static const hifi::ByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary "); +static const hifi::ByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3); static const quint32 FBX_VERSION_2015 = 7400; static const quint32 FBX_VERSION_2016 = 7500; @@ -36,7 +37,7 @@ using FBXNodeList = QList; /// A node within an FBX document. class FBXNode { public: - QByteArray name; + hifi::ByteArray name; QVariantList properties; FBXNodeList children; }; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 9e7f422b40..d5a1f9a562 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -179,7 +179,7 @@ public: void printNode(const FBXNode& node, int indentLevel) { int indentLength = 2; - QByteArray spaces(indentLevel * indentLength, ' '); + hifi::ByteArray spaces(indentLevel * indentLength, ' '); QDebug nodeDebug = qDebug(modelformat); nodeDebug.nospace() << spaces.data() << node.name.data() << ": "; @@ -309,7 +309,7 @@ public: }; bool checkMaterialsHaveTextures(const QHash& materials, - const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { + const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { if (textureFilenames.contains(childID)) { @@ -376,7 +376,7 @@ HFMLight extractLight(const FBXNode& object) { return light; } -QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { +hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { // in order to match the behaviour when loading models from remote URLs // we assume that all external textures are right beside the loaded model // ignoring any relative paths or absolute paths inside of models @@ -384,7 +384,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QString& url) { +HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; QHash modelIDsToNames; @@ -407,11 +407,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr std::map lights; - QVariantHash blendshapeMappings = mapping.value("bs").toHash(); + hifi::VariantHash blendshapeMappings = mapping.value("bs").toHash(); - QMultiHash blendshapeIndices; + QMultiHash blendshapeIndices; for (int i = 0;; i++) { - QByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; + hifi::ByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; if (blendshapeName.isEmpty()) { break; } @@ -454,7 +454,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (subobject.name == "Properties70") { foreach (const FBXNode& subsubobject, subobject.children) { - static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName")); + static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { hfmModel.applicationName = subsubobject.properties.at(4).toString(); @@ -471,8 +471,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr int index = 4; foreach (const FBXNode& subobject, object.children) { if (subobject.name == propertyName) { - static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor"); - static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor"); + static const QVariant UNIT_SCALE_FACTOR = hifi::ByteArray("UnitScaleFactor"); + static const QVariant AMBIENT_COLOR = hifi::ByteArray("AmbientColor"); const auto& subpropName = subobject.properties.at(0); if (subpropName == UNIT_SCALE_FACTOR) { unitScaleFactor = subobject.properties.at(index).toFloat(); @@ -528,7 +528,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QVector blendshapes; foreach (const FBXNode& subobject, object.children) { bool properties = false; - QByteArray propertyName; + hifi::ByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; @@ -541,27 +541,27 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr index = 4; } if (properties) { - static const QVariant ROTATION_ORDER = QByteArray("RotationOrder"); - static const QVariant GEOMETRIC_TRANSLATION = QByteArray("GeometricTranslation"); - static const QVariant GEOMETRIC_ROTATION = QByteArray("GeometricRotation"); - static const QVariant GEOMETRIC_SCALING = QByteArray("GeometricScaling"); - static const QVariant LCL_TRANSLATION = QByteArray("Lcl Translation"); - static const QVariant LCL_ROTATION = QByteArray("Lcl Rotation"); - static const QVariant LCL_SCALING = QByteArray("Lcl Scaling"); - static const QVariant ROTATION_MAX = QByteArray("RotationMax"); - static const QVariant ROTATION_MAX_X = QByteArray("RotationMaxX"); - static const QVariant ROTATION_MAX_Y = QByteArray("RotationMaxY"); - static const QVariant ROTATION_MAX_Z = QByteArray("RotationMaxZ"); - static const QVariant ROTATION_MIN = QByteArray("RotationMin"); - static const QVariant ROTATION_MIN_X = QByteArray("RotationMinX"); - static const QVariant ROTATION_MIN_Y = QByteArray("RotationMinY"); - static const QVariant ROTATION_MIN_Z = QByteArray("RotationMinZ"); - static const QVariant ROTATION_OFFSET = QByteArray("RotationOffset"); - static const QVariant ROTATION_PIVOT = QByteArray("RotationPivot"); - static const QVariant SCALING_OFFSET = QByteArray("ScalingOffset"); - static const QVariant SCALING_PIVOT = QByteArray("ScalingPivot"); - static const QVariant PRE_ROTATION = QByteArray("PreRotation"); - static const QVariant POST_ROTATION = QByteArray("PostRotation"); + static const QVariant ROTATION_ORDER = hifi::ByteArray("RotationOrder"); + static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); + static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation"); + static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling"); + static const QVariant LCL_TRANSLATION = hifi::ByteArray("Lcl Translation"); + static const QVariant LCL_ROTATION = hifi::ByteArray("Lcl Rotation"); + static const QVariant LCL_SCALING = hifi::ByteArray("Lcl Scaling"); + static const QVariant ROTATION_MAX = hifi::ByteArray("RotationMax"); + static const QVariant ROTATION_MAX_X = hifi::ByteArray("RotationMaxX"); + static const QVariant ROTATION_MAX_Y = hifi::ByteArray("RotationMaxY"); + static const QVariant ROTATION_MAX_Z = hifi::ByteArray("RotationMaxZ"); + static const QVariant ROTATION_MIN = hifi::ByteArray("RotationMin"); + static const QVariant ROTATION_MIN_X = hifi::ByteArray("RotationMinX"); + static const QVariant ROTATION_MIN_Y = hifi::ByteArray("RotationMinY"); + static const QVariant ROTATION_MIN_Z = hifi::ByteArray("RotationMinZ"); + static const QVariant ROTATION_OFFSET = hifi::ByteArray("RotationOffset"); + static const QVariant ROTATION_PIVOT = hifi::ByteArray("RotationPivot"); + static const QVariant SCALING_OFFSET = hifi::ByteArray("ScalingOffset"); + static const QVariant SCALING_PIVOT = hifi::ByteArray("ScalingPivot"); + static const QVariant PRE_ROTATION = hifi::ByteArray("PreRotation"); + static const QVariant POST_ROTATION = hifi::ByteArray("PostRotation"); foreach(const FBXNode& property, subobject.children) { const auto& childProperty = property.properties.at(0); if (property.name == propertyName) { @@ -701,8 +701,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr const int MODEL_UV_SCALING_MIN_SIZE = 2; const int CROPPING_MIN_SIZE = 4; if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) { - QByteArray filename = subobject.properties.at(0).toByteArray(); - QByteArray filepath = filename.replace('\\', '/'); + hifi::ByteArray filename = subobject.properties.at(0).toByteArray(); + hifi::ByteArray filepath = filename.replace('\\', '/'); filename = fileOnUrl(filepath, url); _textureFilepaths.insert(getID(object.properties), filepath); _textureFilenames.insert(getID(object.properties), filename); @@ -731,17 +731,17 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr subobject.properties.at(2).value(), subobject.properties.at(3).value())); } else if (subobject.name == "Properties70") { - QByteArray propertyName; + hifi::ByteArray propertyName; int index; propertyName = "P"; index = 4; foreach (const FBXNode& property, subobject.children) { - static const QVariant UV_SET = QByteArray("UVSet"); - static const QVariant CURRENT_TEXTURE_BLEND_MODE = QByteArray("CurrentTextureBlendMode"); - static const QVariant USE_MATERIAL = QByteArray("UseMaterial"); - static const QVariant TRANSLATION = QByteArray("Translation"); - static const QVariant ROTATION = QByteArray("Rotation"); - static const QVariant SCALING = QByteArray("Scaling"); + static const QVariant UV_SET = hifi::ByteArray("UVSet"); + static const QVariant CURRENT_TEXTURE_BLEND_MODE = hifi::ByteArray("CurrentTextureBlendMode"); + static const QVariant USE_MATERIAL = hifi::ByteArray("UseMaterial"); + static const QVariant TRANSLATION = hifi::ByteArray("Translation"); + static const QVariant ROTATION = hifi::ByteArray("Rotation"); + static const QVariant SCALING = hifi::ByteArray("Scaling"); if (property.name == propertyName) { QString v = property.properties.at(0).toString(); if (property.properties.at(0) == UV_SET) { @@ -795,8 +795,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr _textureParams.insert(getID(object.properties), tex); } } else if (object.name == "Video") { - QByteArray filepath; - QByteArray content; + hifi::ByteArray filepath; + hifi::ByteArray content; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { filepath = subobject.properties.at(0).toByteArray(); @@ -816,7 +816,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr foreach (const FBXNode& subobject, object.children) { bool properties = false; - QByteArray propertyName; + hifi::ByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; @@ -833,31 +833,31 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (properties) { std::vector unknowns; - static const QVariant DIFFUSE_COLOR = QByteArray("DiffuseColor"); - static const QVariant DIFFUSE_FACTOR = QByteArray("DiffuseFactor"); - static const QVariant DIFFUSE = QByteArray("Diffuse"); - static const QVariant SPECULAR_COLOR = QByteArray("SpecularColor"); - static const QVariant SPECULAR_FACTOR = QByteArray("SpecularFactor"); - static const QVariant SPECULAR = QByteArray("Specular"); - static const QVariant EMISSIVE_COLOR = QByteArray("EmissiveColor"); - static const QVariant EMISSIVE_FACTOR = QByteArray("EmissiveFactor"); - static const QVariant EMISSIVE = QByteArray("Emissive"); - static const QVariant AMBIENT_FACTOR = QByteArray("AmbientFactor"); - static const QVariant SHININESS = QByteArray("Shininess"); - static const QVariant OPACITY = QByteArray("Opacity"); - static const QVariant MAYA_USE_NORMAL_MAP = QByteArray("Maya|use_normal_map"); - static const QVariant MAYA_BASE_COLOR = QByteArray("Maya|base_color"); - static const QVariant MAYA_USE_COLOR_MAP = QByteArray("Maya|use_color_map"); - static const QVariant MAYA_ROUGHNESS = QByteArray("Maya|roughness"); - static const QVariant MAYA_USE_ROUGHNESS_MAP = QByteArray("Maya|use_roughness_map"); - static const QVariant MAYA_METALLIC = QByteArray("Maya|metallic"); - static const QVariant MAYA_USE_METALLIC_MAP = QByteArray("Maya|use_metallic_map"); - static const QVariant MAYA_EMISSIVE = QByteArray("Maya|emissive"); - static const QVariant MAYA_EMISSIVE_INTENSITY = QByteArray("Maya|emissive_intensity"); - static const QVariant MAYA_USE_EMISSIVE_MAP = QByteArray("Maya|use_emissive_map"); - static const QVariant MAYA_USE_AO_MAP = QByteArray("Maya|use_ao_map"); - static const QVariant MAYA_UV_SCALE = QByteArray("Maya|uv_scale"); - static const QVariant MAYA_UV_OFFSET = QByteArray("Maya|uv_offset"); + static const QVariant DIFFUSE_COLOR = hifi::ByteArray("DiffuseColor"); + static const QVariant DIFFUSE_FACTOR = hifi::ByteArray("DiffuseFactor"); + static const QVariant DIFFUSE = hifi::ByteArray("Diffuse"); + static const QVariant SPECULAR_COLOR = hifi::ByteArray("SpecularColor"); + static const QVariant SPECULAR_FACTOR = hifi::ByteArray("SpecularFactor"); + static const QVariant SPECULAR = hifi::ByteArray("Specular"); + static const QVariant EMISSIVE_COLOR = hifi::ByteArray("EmissiveColor"); + static const QVariant EMISSIVE_FACTOR = hifi::ByteArray("EmissiveFactor"); + static const QVariant EMISSIVE = hifi::ByteArray("Emissive"); + static const QVariant AMBIENT_FACTOR = hifi::ByteArray("AmbientFactor"); + static const QVariant SHININESS = hifi::ByteArray("Shininess"); + static const QVariant OPACITY = hifi::ByteArray("Opacity"); + static const QVariant MAYA_USE_NORMAL_MAP = hifi::ByteArray("Maya|use_normal_map"); + static const QVariant MAYA_BASE_COLOR = hifi::ByteArray("Maya|base_color"); + static const QVariant MAYA_USE_COLOR_MAP = hifi::ByteArray("Maya|use_color_map"); + static const QVariant MAYA_ROUGHNESS = hifi::ByteArray("Maya|roughness"); + static const QVariant MAYA_USE_ROUGHNESS_MAP = hifi::ByteArray("Maya|use_roughness_map"); + static const QVariant MAYA_METALLIC = hifi::ByteArray("Maya|metallic"); + static const QVariant MAYA_USE_METALLIC_MAP = hifi::ByteArray("Maya|use_metallic_map"); + static const QVariant MAYA_EMISSIVE = hifi::ByteArray("Maya|emissive"); + static const QVariant MAYA_EMISSIVE_INTENSITY = hifi::ByteArray("Maya|emissive_intensity"); + static const QVariant MAYA_USE_EMISSIVE_MAP = hifi::ByteArray("Maya|use_emissive_map"); + static const QVariant MAYA_USE_AO_MAP = hifi::ByteArray("Maya|use_ao_map"); + static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale"); + static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset"); static const int MAYA_UV_OFFSET_PROPERTY_LENGTH = 6; static const int MAYA_UV_SCALE_PROPERTY_LENGTH = 6; @@ -1034,7 +1034,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr clusters.insert(getID(object.properties), cluster); } else if (object.properties.last() == "BlendShapeChannel") { - QByteArray name = object.properties.at(1).toByteArray(); + hifi::ByteArray name = object.properties.at(1).toByteArray(); name = name.left(name.indexOf('\0')); if (!blendshapeIndices.contains(name)) { @@ -1071,8 +1071,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } } else if (child.name == "Connections") { - static const QVariant OO = QByteArray("OO"); - static const QVariant OP = QByteArray("OP"); + static const QVariant OO = hifi::ByteArray("OO"); + static const QVariant OP = hifi::ByteArray("OP"); foreach (const FBXNode& connection, child.children) { if (connection.name == "C" || connection.name == "Connect") { if (connection.properties.at(0) == OO) { @@ -1091,7 +1091,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (connection.properties.at(0) == OP) { int counter = 0; - QByteArray type = connection.properties.at(3).toByteArray().toLower(); + hifi::ByteArray type = connection.properties.at(3).toByteArray().toLower(); if (type.contains("DiffuseFactor")) { diffuseFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if ((type.contains("diffuse") && !type.contains("tex_global_diffuse"))) { @@ -1678,8 +1678,8 @@ std::unique_ptr FBXSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer FBXSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { - QBuffer buffer(const_cast(&data)); +HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { + QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); _rootNode = parseFBX(&buffer); diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index 379b1ac743..481f2f4f63 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -15,9 +15,6 @@ #include #include #include -#include -#include -#include #include #include @@ -25,6 +22,7 @@ #include #include +#include #include "FBX.h" #include @@ -114,12 +112,12 @@ public: HFMModel* _hfmModel; /// Reads HFMModel from the supplied model and mapping data. /// \exception QString if an error occurs in parsing - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); - HFMModel* extractHFMModel(const QVariantHash& mapping, const QString& url); + HFMModel* extractHFMModel(const hifi::VariantHash& mapping, const QString& url); static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); QHash meshes; @@ -128,11 +126,11 @@ public: QHash _textureNames; // Hashes the original RelativeFilename of textures - QHash _textureFilepaths; + QHash _textureFilepaths; // Hashes the place to look for textures, in case they are not inlined - QHash _textureFilenames; + QHash _textureFilenames; // Hashes texture content by filepath, in case they are inlined - QHash _textureContent; + QHash _textureContent; QHash _textureParams; diff --git a/libraries/fbx/src/FBXSerializer_Material.cpp b/libraries/fbx/src/FBXSerializer_Material.cpp index b47329e483..8b170eba1b 100644 --- a/libraries/fbx/src/FBXSerializer_Material.cpp +++ b/libraries/fbx/src/FBXSerializer_Material.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include #include @@ -29,7 +28,7 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& materialID) { HFMTexture texture; - const QByteArray& filepath = _textureFilepaths.value(textureID); + const hifi::ByteArray& filepath = _textureFilepaths.value(textureID); texture.content = _textureContent.value(filepath); if (texture.content.isEmpty()) { // the content is not inlined diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index fd1f80425b..f90c4bac6c 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -190,8 +190,8 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me bool isMaterialPerPolygon = false; - static const QVariant BY_VERTICE = QByteArray("ByVertice"); - static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect"); + static const QVariant BY_VERTICE = hifi::ByteArray("ByVertice"); + static const QVariant INDEX_TO_DIRECT = hifi::ByteArray("IndexToDirect"); bool isDracoMesh = false; @@ -321,7 +321,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me } } } else if (child.name == "LayerElementMaterial") { - static const QVariant BY_POLYGON = QByteArray("ByPolygon"); + static const QVariant BY_POLYGON = hifi::ByteArray("ByPolygon"); foreach (const FBXNode& subdata, child.children) { if (subdata.name == "Materials") { materials = getIntVector(subdata); @@ -348,7 +348,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me // load the draco mesh from the FBX and create a draco::Mesh draco::Decoder decoder; draco::DecoderBuffer decodedBuffer; - QByteArray dracoArray = child.properties.at(0).value(); + hifi::ByteArray dracoArray = child.properties.at(0).value(); decodedBuffer.Init(dracoArray.data(), dracoArray.size()); std::unique_ptr dracoMesh(new draco::Mesh()); diff --git a/libraries/fbx/src/FBXSerializer_Node.cpp b/libraries/fbx/src/FBXSerializer_Node.cpp index c982dfc7cb..f9ef84c6f2 100644 --- a/libraries/fbx/src/FBXSerializer_Node.cpp +++ b/libraries/fbx/src/FBXSerializer_Node.cpp @@ -48,10 +48,10 @@ QVariant readBinaryArray(QDataStream& in, int& position) { QVector values; if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) { values.resize(arrayLength); - QByteArray arrayData; + hifi::ByteArray arrayData; if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length - QByteArray compressed(sizeof(quint32) + compressedLength, 0); + hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; @@ -73,11 +73,11 @@ QVariant readBinaryArray(QDataStream& in, int& position) { values.reserve(arrayLength); if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) { // preface encoded data with uncompressed length - QByteArray compressed(sizeof(quint32) + compressedLength, 0); + hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0); *((quint32*)compressed.data()) = qToBigEndian(arrayLength * sizeof(T)); in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; - QByteArray uncompressed = qUncompress(compressed); + hifi::ByteArray uncompressed = qUncompress(compressed); if (uncompressed.isEmpty()) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } @@ -234,7 +234,7 @@ public: }; int nextToken(); - const QByteArray& getDatum() const { return _datum; } + const hifi::ByteArray& getDatum() const { return _datum; } void pushBackToken(int token) { _pushedBackToken = token; } void ungetChar(char ch) { _device->ungetChar(ch); } @@ -242,7 +242,7 @@ public: private: QIODevice* _device; - QByteArray _datum; + hifi::ByteArray _datum; int _pushedBackToken; }; @@ -325,7 +325,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) { expectingDatum = true; } else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) { - QByteArray datum = tokenizer.getDatum(); + hifi::ByteArray datum = tokenizer.getDatum(); if ((token = tokenizer.nextToken()) == ':') { tokenizer.ungetChar(':'); tokenizer.pushBackToken(Tokenizer::DATUM_TOKEN); diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 736e7831c1..b5e87ad759 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -125,18 +125,18 @@ bool GLTFSerializer::getObjectArrayVal(const QJsonObject& object, const QString& return _defined; } -QByteArray GLTFSerializer::setGLBChunks(const QByteArray& data) { +hifi::ByteArray GLTFSerializer::setGLBChunks(const hifi::ByteArray& data) { int byte = 4; int jsonStart = data.indexOf("JSON", Qt::CaseSensitive); int binStart = data.indexOf("BIN", Qt::CaseSensitive); int jsonLength, binLength; - QByteArray jsonLengthChunk, binLengthChunk; + hifi::ByteArray jsonLengthChunk, binLengthChunk; jsonLengthChunk = data.mid(jsonStart - byte, byte); QDataStream tempJsonLen(jsonLengthChunk); tempJsonLen.setByteOrder(QDataStream::LittleEndian); tempJsonLen >> jsonLength; - QByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength); + hifi::ByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength); if (binStart != -1) { binLengthChunk = data.mid(binStart - byte, byte); @@ -567,10 +567,10 @@ bool GLTFSerializer::addTexture(const QJsonObject& object) { return true; } -bool GLTFSerializer::parseGLTF(const QByteArray& data) { +bool GLTFSerializer::parseGLTF(const hifi::ByteArray& data) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - QByteArray jsonChunk = data; + hifi::ByteArray jsonChunk = data; if (_url.toString().endsWith("glb") && data.indexOf("glTF") == 0 && data.contains("JSON")) { jsonChunk = setGLBChunks(data); @@ -734,7 +734,7 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) { return tmat; } -bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { +bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { //Build dependencies QVector> nodeDependencies(_file.nodes.size()); @@ -993,15 +993,15 @@ std::unique_ptr GLTFSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { +HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { _url = url; // Normalize url for local files - QUrl normalizeUrl = DependencyManager::get()->normalizeURL(_url); + hifi::URL normalizeUrl = DependencyManager::get()->normalizeURL(_url); if (normalizeUrl.scheme().isEmpty() || (normalizeUrl.scheme() == "file")) { QString localFileName = PathUtils::expandToLocalDataAbsolutePath(normalizeUrl).toLocalFile(); - _url = QUrl(QFileInfo(localFileName).absoluteFilePath()); + _url = hifi::URL(QFileInfo(localFileName).absoluteFilePath()); } if (parseGLTF(data)) { @@ -1019,15 +1019,15 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas return nullptr; } -bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { +bool GLTFSerializer::readBinary(const QString& url, hifi::ByteArray& outdata) { bool success; if (url.contains("data:application/octet-stream;base64,")) { outdata = requestEmbeddedData(url); success = !outdata.isEmpty(); } else { - QUrl binaryUrl = _url.resolved(url); - std::tie(success, outdata) = requestData(binaryUrl); + hifi::URL binaryUrl = _url.resolved(url); + std::tie(success, outdata) = requestData(binaryUrl); } return success; @@ -1037,16 +1037,16 @@ bool GLTFSerializer::doesResourceExist(const QString& url) { if (_url.isEmpty()) { return false; } - QUrl candidateUrl = _url.resolved(url); + hifi::URL candidateUrl = _url.resolved(url); return DependencyManager::get()->resourceExists(candidateUrl); } -std::tuple GLTFSerializer::requestData(QUrl& url) { +std::tuple GLTFSerializer::requestData(hifi::URL& url) { auto request = DependencyManager::get()->createResourceRequest( nullptr, url, true, -1, "GLTFSerializer::requestData"); if (!request) { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } QEventLoop loop; @@ -1057,17 +1057,17 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { if (request->getResult() == ResourceRequest::Success) { return std::make_tuple(true, request->getData()); } else { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } } -QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { +hifi::ByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { QString binaryUrl = url.split(",")[1]; - return binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); + return binaryUrl.isEmpty() ? hifi::ByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); } -QNetworkReply* GLTFSerializer::request(QUrl& url, bool isTest) { +QNetworkReply* GLTFSerializer::request(hifi::URL& url, bool isTest) { if (!qApp) { return nullptr; } @@ -1098,8 +1098,8 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { if (texture.defined["source"]) { QString url = _file.images[texture.source].uri; - QString fname = QUrl(url).fileName(); - QUrl textureUrl = _url.resolved(url); + QString fname = hifi::URL(url).fileName(); + hifi::URL textureUrl = _url.resolved(url); qCDebug(modelformat) << "fname: " << fname; fbxtex.name = fname; fbxtex.filename = textureUrl.toEncoded(); @@ -1187,7 +1187,7 @@ void GLTFSerializer::setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& mat } template -bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count, +bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType) { QDataStream blobstream(bin); @@ -1244,7 +1244,7 @@ bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count, return true; } template -bool GLTFSerializer::addArrayOfType(const QByteArray& bin, int byteOffset, int count, +bool GLTFSerializer::addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType) { switch (componentType) { diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index a361e09fa6..05dc526f79 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -214,7 +214,7 @@ struct GLTFBufferView { struct GLTFBuffer { int byteLength; //required QString uri; - QByteArray blob; + hifi::ByteArray blob; QMap defined; void dump() { if (defined["byteLength"]) { @@ -705,16 +705,16 @@ public: MediaType getMediaType() const override; std::unique_ptr getFactory() const override; - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; private: GLTFFile _file; - QUrl _url; - QByteArray _glbBinary; + hifi::URL _url; + hifi::ByteArray _glbBinary; glm::mat4 getModelTransform(const GLTFNode& node); - bool buildGeometry(HFMModel& hfmModel, const QUrl& url); - bool parseGLTF(const QByteArray& data); + bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url); + bool parseGLTF(const hifi::ByteArray& data); bool getStringVal(const QJsonObject& object, const QString& fieldname, QString& value, QMap& defined); @@ -733,7 +733,7 @@ private: bool getObjectArrayVal(const QJsonObject& object, const QString& fieldname, QJsonArray& objects, QMap& defined); - QByteArray setGLBChunks(const QByteArray& data); + hifi::ByteArray setGLBChunks(const hifi::ByteArray& data); int getMaterialAlphaMode(const QString& type); int getAccessorType(const QString& type); @@ -760,24 +760,24 @@ private: bool addSkin(const QJsonObject& object); bool addTexture(const QJsonObject& object); - bool readBinary(const QString& url, QByteArray& outdata); + bool readBinary(const QString& url, hifi::ByteArray& outdata); template - bool readArray(const QByteArray& bin, int byteOffset, int count, + bool readArray(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType); template - bool addArrayOfType(const QByteArray& bin, int byteOffset, int count, + bool addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType); void retriangulate(const QVector& in_indices, const QVector& in_vertices, const QVector& in_normals, QVector& out_indices, QVector& out_vertices, QVector& out_normals); - std::tuple requestData(QUrl& url); - QByteArray requestEmbeddedData(const QString& url); + std::tuple requestData(hifi::URL& url); + hifi::ByteArray requestEmbeddedData(const QString& url); - QNetworkReply* request(QUrl& url, bool isTest); + QNetworkReply* request(hifi::URL& url, bool isTest); bool doesResourceExist(const QString& url); diff --git a/libraries/fbx/src/OBJSerializer.cpp b/libraries/fbx/src/OBJSerializer.cpp index 91d3fc7cc0..c2e9c08463 100644 --- a/libraries/fbx/src/OBJSerializer.cpp +++ b/libraries/fbx/src/OBJSerializer.cpp @@ -54,7 +54,7 @@ T& checked_at(QVector& vector, int i) { OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } -const QByteArray OBJTokenizer::getLineAsDatum() { +const hifi::ByteArray OBJTokenizer::getLineAsDatum() { return _device->readLine().trimmed(); } @@ -117,7 +117,7 @@ bool OBJTokenizer::isNextTokenFloat() { if (nextToken() != OBJTokenizer::DATUM_TOKEN) { return false; } - QByteArray token = getDatum(); + hifi::ByteArray token = getDatum(); pushBackToken(OBJTokenizer::DATUM_TOKEN); bool ok; token.toFloat(&ok); @@ -182,7 +182,7 @@ void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID) { // OBJFace // NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just // pairing it with the vertices vector for consistency. -bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { +bool OBJFace::add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { bool ok; int index = vertexIndex.toInt(&ok); if (!ok) { @@ -238,11 +238,11 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } } -bool OBJSerializer::isValidTexture(const QByteArray &filename) { +bool OBJSerializer::isValidTexture(const hifi::ByteArray &filename) { if (_url.isEmpty()) { return false; } - QUrl candidateUrl = _url.resolved(QUrl(filename)); + hifi::URL candidateUrl = _url.resolved(hifi::URL(filename)); return DependencyManager::get()->resourceExists(candidateUrl); } @@ -278,7 +278,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { #endif return; } - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); if (token == "newmtl") { if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { return; @@ -328,8 +328,8 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { } else if (token == "Ks") { currentMaterial.specularColor = tokenizer.getVec3(); } else if ((token == "map_Kd") || (token == "map_Ke") || (token == "map_Ks") || (token == "map_bump") || (token == "bump") || (token == "map_d")) { - const QByteArray textureLine = tokenizer.getLineAsDatum(); - QByteArray filename; + const hifi::ByteArray textureLine = tokenizer.getLineAsDatum(); + hifi::ByteArray filename; OBJMaterialTextureOptions textureOptions; parseTextureLine(textureLine, filename, textureOptions); if (filename.endsWith(".tga")) { @@ -354,7 +354,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) { } } -void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions) { +void OBJSerializer::parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions) { // Texture options reference http://paulbourke.net/dataformats/mtl/ // and https://wikivisually.com/wiki/Material_Template_Library @@ -442,12 +442,12 @@ void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray& } } -std::tuple requestData(QUrl& url) { +std::tuple requestData(hifi::URL& url) { auto request = DependencyManager::get()->createResourceRequest( nullptr, url, true, -1, "(OBJSerializer) requestData"); if (!request) { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } QEventLoop loop; @@ -458,12 +458,12 @@ std::tuple requestData(QUrl& url) { if (request->getResult() == ResourceRequest::Success) { return std::make_tuple(true, request->getData()); } else { - return std::make_tuple(false, QByteArray()); + return std::make_tuple(false, hifi::ByteArray()); } } -QNetworkReply* request(QUrl& url, bool isTest) { +QNetworkReply* request(hifi::URL& url, bool isTest) { if (!qApp) { return nullptr; } @@ -488,7 +488,7 @@ QNetworkReply* request(QUrl& url, bool isTest) { } -bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, +bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts) { FaceGroup faces; HFMMesh& mesh = hfmModel.meshes[0]; @@ -522,7 +522,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m result = false; break; } - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); //qCDebug(modelformat) << token; // we don't support separate objects in the same file, so treat "o" the same as "g". if (token == "g" || token == "o") { @@ -535,7 +535,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { break; } - QByteArray groupName = tokenizer.getDatum(); + hifi::ByteArray groupName = tokenizer.getDatum(); currentGroup = groupName; if (!combineParts) { currentMaterialName = QString("part-") + QString::number(_partCounter++); @@ -544,7 +544,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m if (tokenizer.nextToken(true) != OBJTokenizer::DATUM_TOKEN) { break; } - QByteArray libraryName = tokenizer.getDatum(); + hifi::ByteArray libraryName = tokenizer.getDatum(); librariesSeen[libraryName] = true; // We'll read it later only if we actually need it. } else if (token == "usemtl") { @@ -598,14 +598,14 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m // vertex-index // vertex-index/texture-index // vertex-index/texture-index/surface-normal-index - QByteArray token = tokenizer.getDatum(); + hifi::ByteArray token = tokenizer.getDatum(); auto firstChar = token[0]; // Tokenizer treats line endings as whitespace. Non-digit and non-negative sign indicates done; if (!isdigit(firstChar) && firstChar != '-') { tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); break; } - QList parts = token.split('/'); + QList parts = token.split('/'); assert(parts.count() >= 1); assert(parts.count() <= 3); // If indices are negative relative indices then adjust them to absolute indices based on current vector sizes @@ -626,7 +626,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m } } } - const QByteArray noData {}; + const hifi::ByteArray noData {}; face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices, vertexColors); face.groupName = currentGroup; @@ -661,9 +661,9 @@ std::unique_ptr OBJSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) { +HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); - QBuffer buffer { const_cast(&data) }; + QBuffer buffer { const_cast(&data) }; buffer.open(QIODevice::ReadOnly); auto hfmModelPtr = std::make_shared(); @@ -849,11 +849,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail QString basename = filename.remove(extIndex + 1, sizeof("obj")); preDefinedMaterial.diffuseColor = glm::vec3(1.0f); - QVector extensions = { "jpg", "jpeg", "png", "tga" }; - QByteArray base = basename.toUtf8(), textName = ""; + QVector extensions = { "jpg", "jpeg", "png", "tga" }; + hifi::ByteArray base = basename.toUtf8(), textName = ""; qCDebug(modelformat) << "OBJSerializer looking for default texture"; for (int i = 0; i < extensions.count(); i++) { - QByteArray candidateString = base + extensions[i]; + hifi::ByteArray candidateString = base + extensions[i]; if (isValidTexture(candidateString)) { textName = candidateString; break; @@ -871,11 +871,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash if (needsMaterialLibrary) { foreach (QString libraryName, librariesSeen.keys()) { // Throw away any path part of libraryName, and merge against original url. - QUrl libraryUrl = _url.resolved(QUrl(libraryName).fileName()); + hifi::URL libraryUrl = _url.resolved(hifi::URL(libraryName).fileName()); qCDebug(modelformat) << "OBJSerializer material library" << libraryName; bool success; - QByteArray data; - std::tie(success, data) = requestData(libraryUrl); + hifi::ByteArray data; + std::tie(success, data) = requestData(libraryUrl); if (success) { QBuffer buffer { &data }; buffer.open(QIODevice::ReadOnly); diff --git a/libraries/fbx/src/OBJSerializer.h b/libraries/fbx/src/OBJSerializer.h index c4f8025e66..6fdd95e2c3 100644 --- a/libraries/fbx/src/OBJSerializer.h +++ b/libraries/fbx/src/OBJSerializer.h @@ -25,9 +25,9 @@ public: COMMENT_TOKEN = 0x101 }; int nextToken(bool allowSpaceChar = false); - const QByteArray& getDatum() const { return _datum; } + const hifi::ByteArray& getDatum() const { return _datum; } bool isNextTokenFloat(); - const QByteArray getLineAsDatum(); // some "filenames" have spaces in them + const hifi::ByteArray getLineAsDatum(); // some "filenames" have spaces in them void skipLine() { _device->readLine(); } void pushBackToken(int token) { _pushedBackToken = token; } void ungetChar(char ch) { _device->ungetChar(ch); } @@ -39,7 +39,7 @@ public: private: QIODevice* _device; - QByteArray _datum; + hifi::ByteArray _datum; int _pushedBackToken; QString _comment; }; @@ -52,7 +52,7 @@ public: QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use. QString materialName; // Add one more set of vertex data. Answers true if successful - bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, + bool add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. // Even though HFMMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. @@ -75,11 +75,11 @@ public: glm::vec3 diffuseColor; glm::vec3 specularColor; glm::vec3 emissiveColor; - QByteArray diffuseTextureFilename; - QByteArray specularTextureFilename; - QByteArray emissiveTextureFilename; - QByteArray bumpTextureFilename; - QByteArray opacityTextureFilename; + hifi::ByteArray diffuseTextureFilename; + hifi::ByteArray specularTextureFilename; + hifi::ByteArray emissiveTextureFilename; + hifi::ByteArray bumpTextureFilename; + hifi::ByteArray opacityTextureFilename; OBJMaterialTextureOptions bumpTextureOptions; int illuminationModel; @@ -103,17 +103,17 @@ public: QString currentMaterialName; QHash materials; - HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; private: - QUrl _url; + hifi::URL _url; - QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel, + QHash librariesSeen; + bool parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); - void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions); - bool isValidTexture(const QByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format. + void parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions); + bool isValidTexture(const hifi::ByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format. int _partCounter { 0 }; }; From 612cf43c437a6328bc5832d58d768ee410432729 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 25 Feb 2019 13:03:23 -0800 Subject: [PATCH 008/117] Use HifiTypes.h in VHACDUtil.cpp --- tools/vhacd-util/src/VHACDUtil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 9401da4314..2b18c07c3a 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -42,7 +42,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { return false; } try { - QByteArray fbxContents = fbx.readAll(); + hifi::ByteArray fbxContents = fbx.readAll(); HFMModel::Pointer hfmModel; if (filename.toLower().endsWith(".obj")) { hfmModel = OBJSerializer().read(fbxContents, QVariantHash(), filename); From 86c948f1165a1e2e886489b16b46b5be7dc2f0ce Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 16:03:45 -0800 Subject: [PATCH 009/117] Convert hfmModel and materialMapping fields in model-baker Baker to getters --- libraries/model-baker/src/model-baker/Baker.cpp | 9 +++++++-- libraries/model-baker/src/model-baker/Baker.h | 4 ++-- .../model-networking/src/model-networking/ModelCache.cpp | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 344af1ba8a..fc27756877 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -178,8 +178,13 @@ namespace baker { void Baker::run() { _engine->run(); - hfmModel = _engine->getOutput().get().get0(); - materialMapping = _engine->getOutput().get().get1(); } + hfm::Model::Pointer Baker::getHFMModel() const { + return _engine->getOutput().get().get0(); + } + + MaterialMapping Baker::getMaterialMapping() const { + return _engine->getOutput().get().get1(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 542be0b559..1880aba618 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -28,8 +28,8 @@ namespace baker { void run(); // Outputs, available after run() is called - hfm::Model::Pointer hfmModel; - MaterialMapping materialMapping; + hfm::Model::Pointer getHFMModel() const; + MaterialMapping getMaterialMapping() const; protected: EnginePointer _engine; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 581196b2cc..1d0032ee4c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -341,8 +341,8 @@ void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmMode modelBaker.run(); // Assume ownership of the processed HFMModel - _hfmModel = modelBaker.hfmModel; - _materialMapping = modelBaker.materialMapping; + _hfmModel = modelBaker.getHFMModel(); + _materialMapping = modelBaker.getMaterialMapping(); // Copy materials QHash materialIDAtlas; From 270b96aa8d3c18f453a623d5cf1508bf3f212cc3 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 20 Feb 2019 14:14:15 -0800 Subject: [PATCH 010/117] cleaning up oven --- libraries/baking/src/JSBaker.h | 3 + libraries/baking/src/ModelBaker.cpp | 5 +- libraries/baking/src/ModelBaker.h | 19 +- libraries/baking/src/baking/BakerLibrary.cpp | 50 +- libraries/baking/src/baking/BakerLibrary.h | 2 +- tools/oven/src/BakerCLI.cpp | 85 +-- tools/oven/src/DomainBaker.cpp | 518 ++++++++++++------- tools/oven/src/DomainBaker.h | 18 +- tools/oven/src/ui/ModelBakeWidget.cpp | 19 +- 9 files changed, 431 insertions(+), 288 deletions(-) diff --git a/libraries/baking/src/JSBaker.h b/libraries/baking/src/JSBaker.h index a7c3e62174..764681c71e 100644 --- a/libraries/baking/src/JSBaker.h +++ b/libraries/baking/src/JSBaker.h @@ -25,6 +25,9 @@ public: JSBaker(const QUrl& jsURL, const QString& bakedOutputDir); static bool bakeJS(const QByteArray& inputFile, QByteArray& outputFile); + QString getJSPath() const { return _jsURL.fileName(); } + QString getBakedJSFilePath() const { return _bakedJSFilePath; } + public slots: virtual void bake() override; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 6959a5c455..61eed9f655 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -32,11 +32,12 @@ #endif ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory) : + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : _modelURL(inputModelURL), _bakedOutputDir(bakedOutputDirectory), _originalOutputDir(originalOutputDirectory), - _textureThreadGetter(inputTextureThreadGetter) + _textureThreadGetter(inputTextureThreadGetter), + _hasBeenBaked(hasBeenBaked) { auto tempDir = PathUtils::generateTemporaryDir(); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index dc9d43ad66..0f0cfbe07c 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -30,16 +30,19 @@ using TextureBakerThreadGetter = std::function; using GetMaterialIDCallback = std::function ; -static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; -static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; -static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; +static const QString FST_EXTENSION { ".fst" }; +static const QString BAKED_FST_EXTENSION { ".baked.fst" }; +static const QString FBX_EXTENSION { ".fbx" }; +static const QString BAKED_FBX_EXTENSION { ".baked.fbx" }; +static const QString OBJ_EXTENSION { ".obj" }; +static const QString GLTF_EXTENSION { ".gltf" }; class ModelBaker : public Baker { Q_OBJECT public: ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, - const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual ~ModelBaker(); void initializeOutputDirs(); @@ -59,7 +62,7 @@ protected: void texturesFinished(); void embedTextureMetaData(); void exportScene(); - + FBXNode _rootNode; QHash _textureContentMap; QUrl _modelURL; @@ -79,12 +82,14 @@ private: void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - + TextureBakerThreadGetter _textureThreadGetter; QMultiHash> _bakingTextures; QHash _textureNameMatchCount; QHash _remappedTexturePaths; - bool _pendingErrorEmission{ false }; + bool _pendingErrorEmission { false }; + + bool _hasBeenBaked { false }; }; #endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index a587de97eb..af5e59ebbe 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -14,33 +14,26 @@ #include "../FBXBaker.h" #include "../OBJBaker.h" -QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals) { - // Check if the file pointed to by this URL is a bakeable model, by comparing extensions - auto modelFileName = url.fileName(); +// Check if the file pointed to by this URL is a bakeable model, by comparing extensions +QUrl getBakeableModelURL(const QUrl& url) { + static const std::vector extensionsToBake = { + FST_EXTENSION, + BAKED_FST_EXTENSION, + FBX_EXTENSION, + BAKED_FBX_EXTENSION, + OBJ_EXTENSION, + GLTF_EXTENSION + }; - bool isBakedModel = modelFileName.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive); - bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); - bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); - bool isBakeable = isBakeableFBX || isBakeableOBJ; - - if (isBakeable || (shouldRebakeOriginals && isBakedModel)) { - if (isBakedModel) { - // Grab a URL to the original, that we assume is stored a directory up, in the "original" folder - // with just the fbx extension - qDebug() << "Inferring original URL for baked model URL" << url; - - auto originalFileName = modelFileName; - originalFileName.replace(".baked", ""); - qDebug() << "Original model URL must be present at" << url; - - return url.resolved("../original/" + originalFileName); - } else { - // Grab a clean version of the URL without a query or fragment - return url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + QUrl cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + QString cleanURLString = cleanURL.fileName(); + for (auto& extension : extensionsToBake) { + if (cleanURLString.endsWith(extension, Qt::CaseInsensitive)) { + return cleanURL; } } - qWarning() << "Unknown model type: " << modelFileName; + qWarning() << "Unknown model type: " << url.fileName(); return QUrl(); } @@ -59,11 +52,14 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; std::unique_ptr baker; - - if (filename.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive)) { - baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); - } else if (filename.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive)) { + if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { + //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); + } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); + } else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); + } else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { + //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); } else { qDebug() << "Could not create ModelBaker for url" << bakeableModelURL; } diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index 8739b4e947..e77463b502 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -19,7 +19,7 @@ // Returns either the given model URL, or, if the model is baked and shouldRebakeOriginals is true, // the guessed location of the original model // Returns an empty URL if no bakeable URL found -QUrl getBakeableModelURL(const QUrl& url, bool shouldRebakeOriginals); +QUrl getBakeableModelURL(const QUrl& url); // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index f4e64c3015..f5fffe6ea3 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -37,25 +37,16 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& qDebug() << "Baking file type: " << type; - static const QString MODEL_EXTENSION { "fbx" }; + static const QString MODEL_EXTENSION { "model" }; + static const QString FBX_EXTENSION { "fbx" }; // legacy + static const QString MATERIAL_EXTENSION { "material" }; static const QString SCRIPT_EXTENSION { "js" }; - // check what kind of baker we should be creating - bool isModel = type == MODEL_EXTENSION; - bool isScript = type == SCRIPT_EXTENSION; - - // If the type doesn't match the above, we assume we have a texture, and the type specified is the - // texture usage type (albedo, cubemap, normals, etc.) - auto url = inputUrl.toDisplayString(); - auto idx = url.lastIndexOf('.'); - auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; - bool isSupportedImage = QImageReader::supportedImageFormats().contains(extension.toLatin1()); - _outputPath = outputPath; // create our appropiate baker - if (isModel) { - QUrl bakeableModelURL = getBakeableModelURL(inputUrl, false); + if (type == MODEL_EXTENSION || type == FBX_EXTENSION) { + QUrl bakeableModelURL = getBakeableModelURL(inputUrl); if (!bakeableModelURL.isEmpty()) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); @@ -65,35 +56,49 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _baker->moveToThread(Oven::instance().getNextWorkerThread()); } } - } else if (isScript) { + } else if (type == SCRIPT_EXTENSION) { _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); - } else if (isSupportedImage) { - static const std::unordered_map STRING_TO_TEXTURE_USAGE_TYPE_MAP { - { "default", image::TextureUsage::DEFAULT_TEXTURE }, - { "strict", image::TextureUsage::STRICT_TEXTURE }, - { "albedo", image::TextureUsage::ALBEDO_TEXTURE }, - { "normal", image::TextureUsage::NORMAL_TEXTURE }, - { "bump", image::TextureUsage::BUMP_TEXTURE }, - { "specular", image::TextureUsage::SPECULAR_TEXTURE }, - { "metallic", image::TextureUsage::METALLIC_TEXTURE }, - { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, - { "gloss", image::TextureUsage::GLOSS_TEXTURE }, - { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, - { "cube", image::TextureUsage::CUBE_TEXTURE }, - { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, - { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, - { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, - }; - - auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type); - if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) { - qCDebug(model_baking) << "Unknown texture usage type:" << type; - QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); - } - _baker = std::unique_ptr { new TextureBaker(inputUrl, it->second, outputPath) }; - _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } else if (type == MATERIAL_EXTENSION) { + //_baker = std::unique_ptr { new MaterialBaker(inputUrl, outputPath) }; + //_baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { + // If the type doesn't match the above, we assume we have a texture, and the type specified is the + // texture usage type (albedo, cubemap, normals, etc.) + auto url = inputUrl.toDisplayString(); + auto idx = url.lastIndexOf('.'); + auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + static const std::unordered_map STRING_TO_TEXTURE_USAGE_TYPE_MAP { + { "default", image::TextureUsage::DEFAULT_TEXTURE }, + { "strict", image::TextureUsage::STRICT_TEXTURE }, + { "albedo", image::TextureUsage::ALBEDO_TEXTURE }, + { "normal", image::TextureUsage::NORMAL_TEXTURE }, + { "bump", image::TextureUsage::BUMP_TEXTURE }, + { "specular", image::TextureUsage::SPECULAR_TEXTURE }, + { "metallic", image::TextureUsage::METALLIC_TEXTURE }, + { "roughness", image::TextureUsage::ROUGHNESS_TEXTURE }, + { "gloss", image::TextureUsage::GLOSS_TEXTURE }, + { "emissive", image::TextureUsage::EMISSIVE_TEXTURE }, + { "cube", image::TextureUsage::CUBE_TEXTURE }, + { "skybox", image::TextureUsage::CUBE_TEXTURE }, + { "occlusion", image::TextureUsage::OCCLUSION_TEXTURE }, + { "scattering", image::TextureUsage::SCATTERING_TEXTURE }, + { "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE }, + }; + + auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type); + if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) { + qCDebug(model_baking) << "Unknown texture usage type:" << type; + QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); + } + _baker = std::unique_ptr { new TextureBaker(inputUrl, it->second, outputPath) }; + _baker->moveToThread(Oven::instance().getNextWorkerThread()); + } + } + + if (!_baker) { qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl; QCoreApplication::exit(OVEN_STATUS_CODE_FAIL); return; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 11a38f2f24..3c2f1d77bb 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -27,8 +27,7 @@ DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainNam bool shouldRebakeOriginals) : _localEntitiesFileURL(localModelFileURL), _domainName(domainName), - _baseOutputPath(baseOutputPath), - _shouldRebakeOriginals(shouldRebakeOriginals) + _baseOutputPath(baseOutputPath) { // make sure the destination path has a trailing slash if (!destinationPath.toString().endsWith('/')) { @@ -145,11 +144,139 @@ void DomainBaker::loadLocalFile() { } } -const QString ENTITY_MODEL_URL_KEY = "modelURL"; -const QString ENTITY_SKYBOX_KEY = "skybox"; -const QString ENTITY_SKYBOX_URL_KEY = "url"; -const QString ENTITY_KEYLIGHT_KEY = "keyLight"; -const QString ENTITY_KEYLIGHT_AMBIENT_URL_KEY = "ambientURL"; +void DomainBaker::addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { + // grab a QUrl for the model URL + QUrl bakeableModelURL = getBakeableModelURL(url); + if (!bakeableModelURL.isEmpty()) { + // setup a ModelBaker for this URL, as long as we don't already have one + if (!_modelBakers.contains(bakeableModelURL)) { + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }; + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); + if (baker) { + // 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(bakeableModelURL, baker); + + // move the baker to the baker thread + // and kickoff the bake + baker->moveToThread(Oven::instance().getNextWorkerThread()); + 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(bakeableModelURL, { property, jsonRef }); + } +} + +void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef) { + auto idx = url.lastIndexOf('.'); + auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + // grab a clean version of the URL without a query or fragment + QUrl textureURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a texture baker for this URL, as long as we aren't baking a texture already + if (!_textureBakers.contains(textureURL)) { + // setup a baker for this texture + + QSharedPointer textureBaker { + new TextureBaker(textureURL, type, _contentOutputPath), + &TextureBaker::deleteLater + }; + + // make sure our handler is called when the texture baker is done + connect(textureBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedTextureBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _textureBakers.insert(textureURL, textureBaker); + + // move the baker to a worker thread and kickoff the bake + textureBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(textureBaker.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 texture URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(textureURL, { property, jsonRef }); + } +} + +void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { + // grab a clean version of the URL without a query or fragment + QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // setup a texture baker for this URL, as long as we aren't baking a texture already + if (!_scriptBakers.contains(scriptURL)) { + // setup a baker for this texture + + QSharedPointer scriptBaker { + new JSBaker(scriptURL, _contentOutputPath), + &JSBaker::deleteLater + }; + + // make sure our handler is called when the texture baker is done + connect(scriptBaker.data(), &JSBaker::finished, this, &DomainBaker::handleFinishedScriptBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _scriptBakers.insert(scriptURL, scriptBaker); + + // move the baker to a worker thread and kickoff the bake + scriptBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(scriptBaker.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 texture URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef }); +} + +// All the Entity Properties that can be baked +// *************************************************************************************** + +// Models +const QString MODEL_URL_KEY = "modelURL"; +const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; +const QString GRAP_KEY = "grab"; +const QString EQUIPPABLE_INDICATOR_URL_KEY = "equippableIndicatorURL"; +const QString ANIMATION_KEY = "animation"; +const QString ANIMATION_URL_KEY = "url"; + +// Textures +const QString TEXTURES_KEY = "textures"; +const QString IMAGE_URL_KEY = "imageURL"; +const QString X_TEXTURE_URL_KEY = "xTextureURL"; +const QString Y_TEXTURE_URL_KEY = "yTextureURL"; +const QString Z_TEXTURE_URL_KEY = "zTextureURL"; +const QString AMBIENT_LIGHT_KEY = "ambientLight"; +const QString AMBIENT_URL_KEY = "ambientURL"; +const QString SKYBOX_KEY = "skybox"; +const QString SKYBOX_URL_KEY = "url"; + +// Scripts +const QString SCRIPT_KEY = "script"; +const QString SERVER_SCRIPTS_KEY = "serverScripts"; + +// Materials +const QString MATERIAL_URL_KEY = "materialURL"; +const QString MATERIAL_DATA_KEY = "materialData"; + +// *************************************************************************************** void DomainBaker::enumerateEntities() { qDebug() << "Enumerating" << _entities.size() << "entities from domain"; @@ -159,65 +286,65 @@ void DomainBaker::enumerateEntities() { 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 bakeableModelURL = getBakeableModelURL(entity[ENTITY_MODEL_URL_KEY].toString(), _shouldRebakeOriginals); - - if (!bakeableModelURL.isEmpty()) { - - // setup a ModelBaker for this URL, as long as we don't already have one - if (!_modelBakers.contains(bakeableModelURL)) { - auto getWorkerThreadCallback = []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }; - QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); - if (baker) { - // 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(bakeableModelURL, baker); - - // move the baker to the baker thread - // and kickoff the bake - baker->moveToThread(Oven::instance().getNextWorkerThread()); - 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(bakeableModelURL, *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); -// } -// } + // Models + if (entity.contains(MODEL_URL_KEY)) { + addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } + if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { + // TODO: handle compoundShapeURL + } + if (entity.contains(ANIMATION_KEY)) { + auto animationObject = entity[ANIMATION_KEY].toObject(); + if (animationObject.contains(ANIMATION_URL_KEY)) { + addModelBaker(ANIMATION_KEY + "." + ANIMATION_URL_KEY, animationObject[ANIMATION_URL_KEY].toString(), *it); + } + } + if (entity.contains(GRAP_KEY)) { + auto grabObject = entity[GRAP_KEY].toObject(); + if (grabObject.contains(EQUIPPABLE_INDICATOR_URL_KEY)) { + addModelBaker(GRAP_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it); + } + } + + // Textures + if (entity.contains(TEXTURES_KEY)) { + // TODO: the textures property is treated differently for different entity types + } + if (entity.contains(IMAGE_URL_KEY)) { + addTextureBaker(IMAGE_URL_KEY, entity[IMAGE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(X_TEXTURE_URL_KEY)) { + addTextureBaker(X_TEXTURE_URL_KEY, entity[X_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(Y_TEXTURE_URL_KEY)) { + addTextureBaker(Y_TEXTURE_URL_KEY, entity[Y_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(Z_TEXTURE_URL_KEY)) { + addTextureBaker(Z_TEXTURE_URL_KEY, entity[Z_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + if (entity.contains(AMBIENT_LIGHT_KEY)) { + auto ambientLight = entity[AMBIENT_LIGHT_KEY].toObject(); + if (ambientLight.contains(AMBIENT_URL_KEY)) { + addTextureBaker(AMBIENT_LIGHT_KEY + "." + AMBIENT_URL_KEY, ambientLight[AMBIENT_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + } + } + if (entity.contains(SKYBOX_KEY)) { + auto skybox = entity[SKYBOX_KEY].toObject(); + if (skybox.contains(SKYBOX_URL_KEY)) { + addTextureBaker(SKYBOX_KEY + "." + SKYBOX_URL_KEY, skybox[SKYBOX_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it); + } + } + + // Scripts + if (entity.contains(SCRIPT_KEY)) { + addScriptBaker(SCRIPT_KEY, entity[SCRIPT_KEY].toString(), *it); + } + if (entity.contains(SERVER_SCRIPTS_KEY)) { + // TODO: serverScripts can be multiple scripts, need to handle that + } + + // Materials + // TODO } } @@ -225,48 +352,6 @@ void DomainBaker::enumerateEntities() { emit bakeProgress(0, _totalNumberOfSubBakes); } -void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { - - auto skyboxFileName = skyboxURL.fileName(); - - static const QStringList BAKEABLE_SKYBOX_EXTENSIONS { - ".jpg", ".png", ".gif", ".bmp", ".pbm", ".pgm", ".ppm", ".xbm", ".xpm", ".svg" - }; - auto completeLowerExtension = skyboxFileName.mid(skyboxFileName.indexOf('.')).toLower(); - - if (BAKEABLE_SKYBOX_EXTENSIONS.contains(completeLowerExtension)) { - // grab a clean version of the URL without a query or fragment - skyboxURL = skyboxURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - - // setup a texture baker for this URL, as long as we aren't baking a skybox already - if (!_skyboxBakers.contains(skyboxURL)) { - // setup a baker for this skybox - - QSharedPointer skyboxBaker { - new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath), - &TextureBaker::deleteLater - }; - - // make sure our handler is called when the skybox baker is done - connect(skyboxBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedSkyboxBaker); - - // insert it into our bakers hash so we hold a strong pointer to it - _skyboxBakers.insert(skyboxURL, skyboxBaker); - - // move the baker to a worker thread and kickoff the bake - skyboxBaker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(skyboxBaker.data(), "bake"); - - // keep track of the total number of baking entities - ++_totalNumberOfSubBakes; - } - - // add this QJsonValueRef to our multi hash so that it can re-write the skybox URL - // to the baked version once the baker is complete - _entitiesNeedingRewrite.insert(skyboxURL, entity); - } -} - void DomainBaker::handleFinishedModelBaker() { auto baker = qobject_cast(sender()); @@ -275,62 +360,51 @@ void DomainBaker::handleFinishedModelBaker() { // this FBXBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getModelURL(); - // enumerate the QJsonRef values for the URL of this FBX from our multi hash of + // setup a new URL using the prefix we were passed + auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); + if (relativeFBXFilePath.startsWith("/")) { + relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + } + QUrl newURL = _destinationPath.resolved(relativeFBXFilePath); + + // enumerate the QJsonRef values for the URL of this model from our multi hash of // entity objects needing a URL re-write - for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getModelURL())) { - + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getModelURL())) { + QString property = propertyEntityPair.first; // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL - auto entity = entityValue.toObject(); + auto entity = propertyEntityPair.second.toObject(); - // grab the old URL - QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); - // setup a new URL using the prefix we were passed - auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); - if (relativeFBXFilePath.startsWith("/")) { - relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; } - QUrl newModelURL = _destinationPath.resolved(relativeFBXFilePath); - // 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(relativeFBXFilePath); - 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; + propertyEntityPair.second = entity; } } else { // this model failed to bake - this doesn't fail the entire bake but we need to add @@ -352,7 +426,7 @@ void DomainBaker::handleFinishedModelBaker() { } } -void DomainBaker::handleFinishedSkyboxBaker() { +void DomainBaker::handleFinishedTextureBaker() { auto baker = qobject_cast(sender()); if (baker) { @@ -360,36 +434,46 @@ void DomainBaker::handleFinishedSkyboxBaker() { // 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 + auto newURL = _destinationPath.resolved(baker->getMetaTextureFileName()); + + // enumerate the QJsonRef values for the URL of this texture from our multi hash of // entity objects needing a URL re-write - for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getTextureURL())) { + QString property = propertyEntityPair.first; // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL - auto entity = entityValue.toObject(); + auto entity = propertyEntityPair.second.toObject(); - if (entity.contains(ENTITY_SKYBOX_KEY)) { - auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject(); + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); - 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; - } - } - } + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); - if (entity.contains(ENTITY_KEYLIGHT_KEY)) { - auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject(); + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); - 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; - } - } + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; } // replace our temp object with the value referenced by our QJsonValueRef - entityValue = entity; + propertyEntityPair.second = entity; } } else { // this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from @@ -401,7 +485,7 @@ void DomainBaker::handleFinishedSkyboxBaker() { _entitiesNeedingRewrite.remove(baker->getTextureURL()); // drop our shared pointer to this baker so that it gets cleaned up - _skyboxBakers.remove(baker->getTextureURL()); + _textureBakers.remove(baker->getTextureURL()); // emit progress to tell listeners how many models we have baked emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); @@ -411,23 +495,72 @@ void DomainBaker::handleFinishedSkyboxBaker() { } } -bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) { - // grab the old skybox URL - QUrl oldSkyboxURL { urlValue.toString() }; +void DomainBaker::handleFinishedScriptBaker() { + auto baker = qobject_cast(sender()); - if (oldSkyboxURL.matches(baker->getTextureURL(), QUrl::RemoveQuery | QUrl::RemoveFragment)) { - // change the URL to point to the baked texture with its original query and fragment + if (baker) { + if (!baker->hasErrors()) { + // this FBXBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getJSPath(); - auto newSkyboxURL = _destinationPath.resolved(baker->getMetaTextureFileName()); - newSkyboxURL.setQuery(oldSkyboxURL.query()); - newSkyboxURL.setFragment(oldSkyboxURL.fragment()); - newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo()); + auto newURL = _destinationPath.resolved(baker->getBakedJSFilePath()); - urlValue = newSkyboxURL.toString(); + // enumerate the QJsonRef values for the URL of this script from our multi hash of + // entity objects needing a URL re-write + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getJSPath())) { + QString property = propertyEntityPair.first; + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = propertyEntityPair.second.toObject(); - return true; - } else { - return false; + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); + + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old model URL + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; + } + + // replace our temp object with the value referenced by our QJsonValueRef + propertyEntityPair.second = 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->getJSPath()); + + // drop our shared pointer to this baker so that it gets cleaned up + _scriptBakers.remove(baker->getJSPath()); + + // 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(); } } @@ -480,4 +613,3 @@ void DomainBaker::writeNewEntitiesFile() { qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; } - diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index e0286a51ff..2a5abb4ca6 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -18,8 +18,9 @@ #include #include "Baker.h" -#include "FBXBaker.h" +#include "ModelBaker.h" #include "TextureBaker.h" +#include "JSBaker.h" class DomainBaker : public Baker { Q_OBJECT @@ -38,7 +39,8 @@ signals: private slots: virtual void bake() override; void handleFinishedModelBaker(); - void handleFinishedSkyboxBaker(); + void handleFinishedTextureBaker(); + void handleFinishedScriptBaker(); private: void setupOutputFolder(); @@ -47,9 +49,6 @@ private: void checkIfRewritingComplete(); void writeNewEntitiesFile(); - void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity); - bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker); - QUrl _localEntitiesFileURL; QString _domainName; QString _baseOutputPath; @@ -62,14 +61,17 @@ private: QJsonArray _entities; QHash> _modelBakers; - QHash> _skyboxBakers; + QHash> _textureBakers; + QHash> _scriptBakers; - QMultiHash _entitiesNeedingRewrite; + QMultiHash> _entitiesNeedingRewrite; int _totalNumberOfSubBakes { 0 }; int _completedSubBakes { 0 }; - bool _shouldRebakeOriginals { false }; + void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); + void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); + void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 5ac9b43348..8f8e068b50 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -116,7 +116,7 @@ void ModelBakeWidget::chooseFileButtonClicked() { startDir = QDir::homePath(); } - auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj)"); + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj *.gltf *.fst)"); if (!selectedFiles.isEmpty()) { // set the contents of the model file text box to be the path to the selected file @@ -165,21 +165,20 @@ void ModelBakeWidget::bakeButtonClicked() { return; } + // make sure we have a valid output directory + QDir outputDirectory(_outputDirLineEdit->text()); + if (!outputDirectory.exists()) { + QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); + 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); - // make sure we have a valid output directory - QDir outputDirectory(_outputDirLineEdit->text()); - if (!outputDirectory.exists()) { - QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); - return; - } - - QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL), false); - + QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL)); if (!bakeableModelURL.isEmpty()) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); From 162573bc634c722650c58827269ed0f4d800d638 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 20 Feb 2019 16:50:26 -0800 Subject: [PATCH 011/117] enable js baking from non-local file --- libraries/baking/src/JSBaker.cpp | 78 ++++++++++++++++++++++---- libraries/baking/src/JSBaker.h | 12 +++- tools/oven/src/DomainBaker.cpp | 11 ++-- tools/oven/src/DomainBaker.h | 3 +- tools/oven/src/ui/DomainBakeWidget.cpp | 7 +-- tools/oven/src/ui/DomainBakeWidget.h | 1 - 6 files changed, 87 insertions(+), 25 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index b19336f4ca..82d482967b 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -11,9 +11,11 @@ #include "JSBaker.h" -#include +#include -#include "Baker.h" +#include +#include +#include const int ASCII_CHARACTERS_UPPER_LIMIT = 126; @@ -21,25 +23,79 @@ JSBaker::JSBaker(const QUrl& jsURL, const QString& bakedOutputDir) : _jsURL(jsURL), _bakedOutputDir(bakedOutputDir) { - } void JSBaker::bake() { qCDebug(js_baking) << "JS Baker " << _jsURL << "bake starting"; - // Import file to start baking - QFile jsFile(_jsURL.toLocalFile()); - if (!jsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - handleError("Error opening " + _jsURL.fileName() + " for reading"); - return; - } + // once our texture is loaded, kick off a the processing + connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript); + if (_jsURL.isEmpty()) { + // first load the texture (either locally or remotely) + loadScript(); + } else { + // we already have a texture passed to us, use that + emit originalScriptLoaded(); + } +} + +void JSBaker::loadScript() { + // check if the texture is local or first needs to be downloaded + if (_jsURL.isLocalFile()) { + // load up the local file + QFile localScript(_jsURL.toLocalFile()); + if (!localScript.open(QIODevice::ReadOnly | QIODevice::Text)) { + handleError("Error opening " + _jsURL.fileName() + " for reading"); + return; + } + + _originalScript = localScript.readAll(); + + emit originalScriptLoaded(); + } 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(_jsURL); + + qCDebug(js_baking) << "Downloading" << _jsURL; + + // kickoff the download, wait for slot to tell us it is done + auto networkReply = networkAccessManager.get(networkRequest); + connect(networkReply, &QNetworkReply::finished, this, &JSBaker::handleScriptNetworkReply); + } +} + +void JSBaker::handleScriptNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(js_baking) << "Downloaded texture" << _jsURL; + + // store the original texture so it can be passed along for the bake + _originalScript = requestReply->readAll(); + + emit originalScriptLoaded(); + } else { + // add an error to our list stating that this texture could not be downloaded + handleError("Error downloading " + _jsURL.toString() + " - " + requestReply->errorString()); + } +} + +void JSBaker::processScript() { // Read file into an array - QByteArray inputJS = jsFile.readAll(); QByteArray outputJS; // Call baking on inputJS and store result in outputJS - bool success = bakeJS(inputJS, outputJS); + bool success = bakeJS(_originalScript, outputJS); if (!success) { qCDebug(js_baking) << "Bake Failed"; handleError("Unterminated multi-line comment"); diff --git a/libraries/baking/src/JSBaker.h b/libraries/baking/src/JSBaker.h index 764681c71e..7eda85fa6d 100644 --- a/libraries/baking/src/JSBaker.h +++ b/libraries/baking/src/JSBaker.h @@ -25,14 +25,24 @@ public: JSBaker(const QUrl& jsURL, const QString& bakedOutputDir); static bool bakeJS(const QByteArray& inputFile, QByteArray& outputFile); - QString getJSPath() const { return _jsURL.fileName(); } + QString getJSPath() const { return _jsURL.toDisplayString(); } QString getBakedJSFilePath() const { return _bakedJSFilePath; } public slots: virtual void bake() override; +signals: + void originalScriptLoaded(); + +private slots: + void processScript(); + private: + void loadScript(); + void handleScriptNetworkReply(); + QUrl _jsURL; + QByteArray _originalScript; QString _bakedOutputDir; QString _bakedJSFilePath; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 3c2f1d77bb..6f94b455d9 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -23,8 +23,7 @@ #include "baking/BakerLibrary.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, - const QString& baseOutputPath, const QUrl& destinationPath, - bool shouldRebakeOriginals) : + const QString& baseOutputPath, const QUrl& destinationPath) : _localEntitiesFileURL(localModelFileURL), _domainName(domainName), _baseOutputPath(baseOutputPath) @@ -178,7 +177,8 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs } void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef) { - auto idx = url.lastIndexOf('.'); + QString cleanURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + auto idx = cleanURL.lastIndexOf('.'); auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { @@ -211,6 +211,8 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i // add this QJsonValueRef to our multi hash so that it can re-write the texture URL // to the baked version once the baker is complete _entitiesNeedingRewrite.insert(textureURL, { property, jsonRef }); + } else { + qDebug() << "Texture extension not supported: " << extension; } } @@ -551,6 +553,7 @@ void DomainBaker::handleFinishedScriptBaker() { } // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getJSPath()); // drop our shared pointer to this baker so that it gets cleaned up @@ -611,5 +614,5 @@ void DomainBaker::writeNewEntitiesFile() { return; } - qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath; + qDebug() << "Exported baked entities file to" << bakedEntitiesFilePath; } diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 2a5abb4ca6..2a9522143e 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -29,8 +29,7 @@ public: // 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, - bool shouldRebakeOriginals = false); + const QString& baseOutputPath, const QUrl& destinationPath); signals: void allModelsFinished(); diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 1121041e39..23074e775e 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -126,10 +126,6 @@ void DomainBakeWidget::setupUI() { // start a new row for the next component ++rowIndex; - // setup a checkbox to allow re-baking of original assets - _rebakeOriginalsCheckBox = new QCheckBox("Re-bake originals"); - gridLayout->addWidget(_rebakeOriginalsCheckBox, rowIndex, 0); - // add a button that will kickoff the bake QPushButton* bakeButton = new QPushButton("Bake"); connect(bakeButton, &QPushButton::clicked, this, &DomainBakeWidget::bakeButtonClicked); @@ -211,8 +207,7 @@ void DomainBakeWidget::bakeButtonClicked() { auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text()); auto domainBaker = std::unique_ptr { new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), - outputDirectory.absolutePath(), _destinationPathLineEdit->text(), - _rebakeOriginalsCheckBox->isChecked()) + outputDirectory.absolutePath(), _destinationPathLineEdit->text()) }; // make sure we hear from the baker when it is done diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index a6f26b3731..0a1d613912 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -45,7 +45,6 @@ private: QLineEdit* _entitiesFileLineEdit; QLineEdit* _outputDirLineEdit; QLineEdit* _destinationPathLineEdit; - QCheckBox* _rebakeOriginalsCheckBox; Setting::Handle _domainNameSetting; Setting::Handle _exportDirectory; From 7fc9a3fdb65848527c661ec1ec6aac13fe4f0469 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 21 Feb 2019 12:12:09 -0800 Subject: [PATCH 012/117] wip --- libraries/baking/src/JSBaker.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index 82d482967b..c43c5ad00a 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -28,20 +28,20 @@ JSBaker::JSBaker(const QUrl& jsURL, const QString& bakedOutputDir) : void JSBaker::bake() { qCDebug(js_baking) << "JS Baker " << _jsURL << "bake starting"; - // once our texture is loaded, kick off a the processing + // once our script is loaded, kick off a the processing connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript); if (_jsURL.isEmpty()) { - // first load the texture (either locally or remotely) + // first load the script (either locally or remotely) loadScript(); } else { - // we already have a texture passed to us, use that + // we already have a script passed to us, use that emit originalScriptLoaded(); } } void JSBaker::loadScript() { - // check if the texture is local or first needs to be downloaded + // check if the script is local or first needs to be downloaded if (_jsURL.isLocalFile()) { // load up the local file QFile localScript(_jsURL.toLocalFile()); @@ -78,14 +78,14 @@ void JSBaker::handleScriptNetworkReply() { auto requestReply = qobject_cast(sender()); if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(js_baking) << "Downloaded texture" << _jsURL; + qCDebug(js_baking) << "Downloaded script" << _jsURL; - // store the original texture so it can be passed along for the bake + // store the original script so it can be passed along for the bake _originalScript = requestReply->readAll(); emit originalScriptLoaded(); } else { - // add an error to our list stating that this texture could not be downloaded + // add an error to our list stating that this script could not be downloaded handleError("Error downloading " + _jsURL.toString() + " - " + requestReply->errorString()); } } @@ -139,7 +139,10 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { in >> currentCharacter; + qDebug() << "boop" << inputFile; + while (!in.atEnd()) { + qDebug() << "boop2" << currentCharacter << nextCharacter << previousCharacter; in >> nextCharacter; if (currentCharacter == '\r') { @@ -228,6 +231,8 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { out << currentCharacter; } + qDebug() << "boop3" << outputFile; + // Successful bake. Return true return true; } From 4965adbc2f2671466d72f46adf56e04bac4ae6e4 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Feb 2019 18:12:11 -0800 Subject: [PATCH 013/117] bake js and collision hull --- libraries/baking/src/JSBaker.cpp | 7 +------ tools/oven/src/DomainBaker.cpp | 35 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index c43c5ad00a..e5682cde20 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -31,7 +31,7 @@ void JSBaker::bake() { // once our script is loaded, kick off a the processing connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript); - if (_jsURL.isEmpty()) { + if (_originalScript.isEmpty()) { // first load the script (either locally or remotely) loadScript(); } else { @@ -139,10 +139,7 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { in >> currentCharacter; - qDebug() << "boop" << inputFile; - while (!in.atEnd()) { - qDebug() << "boop2" << currentCharacter << nextCharacter << previousCharacter; in >> nextCharacter; if (currentCharacter == '\r') { @@ -231,8 +228,6 @@ bool JSBaker::bakeJS(const QByteArray& inputFile, QByteArray& outputFile) { out << currentCharacter; } - qDebug() << "boop3" << outputFile; - // Successful bake. Return true return true; } diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 6f94b455d9..2eb2c8a36b 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -293,7 +293,9 @@ void DomainBaker::enumerateEntities() { addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { - // TODO: handle compoundShapeURL + // TODO: this could be optimized so that we don't do the full baking pass for collision shapes, + // but we have to handle the case where it's also used as a modelURL somewhere + addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); } if (entity.contains(ANIMATION_KEY)) { auto animationObject = entity[ANIMATION_KEY].toObject(); @@ -359,7 +361,7 @@ void DomainBaker::handleFinishedModelBaker() { if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this ModelBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getModelURL(); // setup a new URL using the prefix we were passed @@ -433,7 +435,7 @@ void DomainBaker::handleFinishedTextureBaker() { if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this TextureBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getTextureURL(); auto newURL = _destinationPath.resolved(baker->getMetaTextureFileName()); @@ -449,7 +451,7 @@ void DomainBaker::handleFinishedTextureBaker() { // grab the old URL QUrl oldURL = entity[property].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old texture URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -464,7 +466,7 @@ void DomainBaker::handleFinishedTextureBaker() { auto oldObject = entity[propertySplit[0]].toObject(); QUrl oldURL = oldObject[propertySplit[1]].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old texture URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -478,8 +480,8 @@ void DomainBaker::handleFinishedTextureBaker() { propertyEntityPair.second = 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 + // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the texture to our warnings _warningList << baker->getWarnings(); } @@ -489,10 +491,10 @@ void DomainBaker::handleFinishedTextureBaker() { // drop our shared pointer to this baker so that it gets cleaned up _textureBakers.remove(baker->getTextureURL()); - // emit progress to tell listeners how many models we have baked + // emit progress to tell listeners how many textures 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 + // check if this was the last texture we needed to re-write and if we are done now checkIfRewritingComplete(); } } @@ -502,7 +504,7 @@ void DomainBaker::handleFinishedScriptBaker() { if (baker) { if (!baker->hasErrors()) { - // this FBXBaker is done and everything went according to plan + // this JSBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getJSPath(); auto newURL = _destinationPath.resolved(baker->getBakedJSFilePath()); @@ -518,7 +520,7 @@ void DomainBaker::handleFinishedScriptBaker() { // grab the old URL QUrl oldURL = entity[property].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old script URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -533,7 +535,7 @@ void DomainBaker::handleFinishedScriptBaker() { auto oldObject = entity[propertySplit[0]].toObject(); QUrl oldURL = oldObject[propertySplit[1]].toString(); - // copy the fragment and query, and user info from the old model URL + // copy the fragment and query, and user info from the old script URL newURL.setQuery(oldURL.query()); newURL.setFragment(oldURL.fragment()); newURL.setUserInfo(oldURL.userInfo()); @@ -547,22 +549,21 @@ void DomainBaker::handleFinishedScriptBaker() { propertyEntityPair.second = 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 + // this script failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the script to our warnings _warningList << baker->getErrors(); } // remove the baked URL from the multi hash of entities needing a re-write - _entitiesNeedingRewrite.remove(baker->getJSPath()); // drop our shared pointer to this baker so that it gets cleaned up _scriptBakers.remove(baker->getJSPath()); - // emit progress to tell listeners how many models we have baked + // emit progress to tell listeners how many scripts 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 + // check if this was the last script we needed to re-write and if we are done now checkIfRewritingComplete(); } } From 94de0c12bc8a8ddbd1e771abfdf0772790d9c00d Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 26 Feb 2019 15:02:13 -0800 Subject: [PATCH 014/117] working on material baker --- libraries/baking/CMakeLists.txt | 2 +- libraries/baking/src/MaterialBaker.cpp | 118 +++++++++++++++ libraries/baking/src/MaterialBaker.h | 58 ++++++++ .../src/MaterialBakingLoggingCategory.cpp | 14 ++ .../src/MaterialBakingLoggingCategory.h | 19 +++ .../src/graphics-scripting/Forward.h | 2 + .../GraphicsScriptingInterface.cpp | 87 +++++++++-- .../graphics-scripting/ScriptableModel.cpp | 115 ++++++++------- tools/oven/CMakeLists.txt | 2 +- tools/oven/src/BakerCLI.cpp | 5 +- tools/oven/src/DomainBaker.cpp | 139 +++++++++++++++++- tools/oven/src/DomainBaker.h | 4 + tools/oven/src/Oven.cpp | 4 + 13 files changed, 491 insertions(+), 78 deletions(-) create mode 100644 libraries/baking/src/MaterialBaker.cpp create mode 100644 libraries/baking/src/MaterialBaker.h create mode 100644 libraries/baking/src/MaterialBakingLoggingCategory.cpp create mode 100644 libraries/baking/src/MaterialBakingLoggingCategory.h diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index cce76f152f..2fa4c86691 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME baking) setup_hifi_library(Concurrent) -link_hifi_libraries(shared graphics networking ktx image fbx) +link_hifi_libraries(shared shaders graphics networking material-networking ktx image fbx) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp new file mode 100644 index 0000000000..3427663b09 --- /dev/null +++ b/libraries/baking/src/MaterialBaker.cpp @@ -0,0 +1,118 @@ +// +// MaterialBaker.cpp +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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 "MaterialBaker.h" + +#include "QJsonObject" +#include "QJsonDocument" + +#include "MaterialBakingLoggingCategory.h" + +#include +#include + +MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) : + _materialData(materialData), + _isURL(isURL), + _bakedOutputDir(bakedOutputDir) +{ +} + +void MaterialBaker::bake() { + qDebug(material_baking) << "Material Baker" << _materialData << "bake starting"; + + // once our script is loaded, kick off a the processing + connect(this, &MaterialBaker::originalMaterialLoaded, this, &MaterialBaker::processMaterial); + + if (!_materialResource) { + // first load the material (either locally or remotely) + loadMaterial(); + } else { + // we already have a material passed to us, use that + if (_materialResource->isLoaded()) { + emit originalMaterialLoaded(); + } else { + connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + } + } +} + +void MaterialBaker::loadMaterial() { + if (!_isURL) { + qCDebug(material_baking) << "Loading local material" << _materialData; + + _materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource()); + // TODO: add baseURL to allow these to reference relative files next to them + _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromVariant(_materialData), QUrl()); + } else { + qCDebug(material_baking) << "Downloading material" << _materialData; + _materialResource = MaterialCache::instance().getMaterial(_materialData); + } + + if (_materialResource) { + if (_materialResource->isLoaded()) { + emit originalMaterialLoaded(); + } else { + connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + } + } else { + handleError("Error loading " + _materialData); + } +} + +void MaterialBaker::processMaterial() { + if (!_materialResource || _materialResource->parsedMaterials.networkMaterials.size() == 0) { + handleError("Error processing " + _materialData); + } + + _numTexturesToLoad = _materialResource->parsedMaterials.networkMaterials.size(); + _numTexturesLoaded = 0; + + for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { + if (networkMaterial.second) { + auto textureMaps = networkMaterial.second->getTextureMaps(); + for (auto textureMap : textureMaps) { + if (textureMap.second && textureMap.second->getTextureSource()) { + auto texture = textureMap.second->getTextureSource(); + graphics::Material::MapChannel mapChannel = textureMap.first; + + qDebug() << "boop" << mapChannel << texture->getUrl(); + } + } + } + } +} + +void MaterialBaker::outputMaterial() { + //if (_isURL) { + // auto fileName = _materialData; + // auto baseName = fileName.left(fileName.lastIndexOf('.')); + // auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION; + + // _bakedMaterialData = _bakedOutputDir + "/" + bakedFilename; + + // QFile bakedFile; + // bakedFile.setFileName(_bakedMaterialData); + // if (!bakedFile.open(QIODevice::WriteOnly)) { + // handleError("Error opening " + _bakedMaterialData + " for writing"); + // return; + // } + + // bakedFile.write(outputMaterial); + + // // Export successful + // _outputFiles.push_back(_bakedMaterialData); + // qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData; + //} + + // emit signal to indicate the material baking is finished + emit finished(); +} diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h new file mode 100644 index 0000000000..6113515b81 --- /dev/null +++ b/libraries/baking/src/MaterialBaker.h @@ -0,0 +1,58 @@ +// +// MaterialBaker.h +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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_MaterialBaker_h +#define hifi_MaterialBaker_h + +#include "Baker.h" + +#include "TextureBaker.h" + +#include + +static const QString BAKED_MATERIAL_EXTENSION = ".baked.json"; + +class MaterialBaker : public Baker { + Q_OBJECT +public: + MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir); + + QString getMaterialData() const { return _materialData; } + bool isURL() const { return _isURL; } + QString getBakedMaterialData() const { return _bakedMaterialData; } + +public slots: + virtual void bake() override; + +signals: + void originalMaterialLoaded(); + +private slots: + void processMaterial(); + void outputMaterial(); + +private: + void loadMaterial(); + + QString _materialData; + bool _isURL; + + NetworkMaterialResourcePointer _materialResource; + size_t _numTexturesToLoad { 0 }; + size_t _numTexturesLoaded { 0 }; + + QHash> _textureBakers; + + QString _bakedOutputDir; + QString _bakedMaterialData; +}; + +#endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/MaterialBakingLoggingCategory.cpp b/libraries/baking/src/MaterialBakingLoggingCategory.cpp new file mode 100644 index 0000000000..75c0e6319c --- /dev/null +++ b/libraries/baking/src/MaterialBakingLoggingCategory.cpp @@ -0,0 +1,14 @@ +// +// MaterialBakingLoggingCategory.cpp +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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 "MaterialBakingLoggingCategory.h" + +Q_LOGGING_CATEGORY(material_baking, "hifi.material-baking"); diff --git a/libraries/baking/src/MaterialBakingLoggingCategory.h b/libraries/baking/src/MaterialBakingLoggingCategory.h new file mode 100644 index 0000000000..768bd9d769 --- /dev/null +++ b/libraries/baking/src/MaterialBakingLoggingCategory.h @@ -0,0 +1,19 @@ +// +// MaterialBakingLoggingCategory.h +// libraries/baking/src +// +// Created by Sam Gondelman on 2/26/2019 +// Copyright 2019 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_MaterialBakingLoggingCategory_h +#define hifi_MaterialBakingLoggingCategory_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(material_baking) + +#endif // hifi_MaterialBakingLoggingCategory_h diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h index 747788aef8..d2d330167d 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -96,6 +96,8 @@ namespace scriptable { bool defaultFallthrough; std::unordered_map propertyFallthroughs; // not actually exposed to script + + graphics::MaterialKey key { 0 }; }; /**jsdoc diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 848f9d42ac..1fd7ad9df5 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -364,20 +364,81 @@ namespace scriptable { obj.setProperty("model", material.model); const QScriptValue FALLTHROUGH("fallthrough"); - obj.setProperty("opacity", material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT) ? FALLTHROUGH : material.opacity); - obj.setProperty("roughness", material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT) ? FALLTHROUGH : material.roughness); - obj.setProperty("metallic", material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT) ? FALLTHROUGH : material.metallic); - obj.setProperty("scattering", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT) ? FALLTHROUGH : material.scattering); - obj.setProperty("unlit", material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT) ? FALLTHROUGH : material.unlit); - obj.setProperty("emissive", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.emissive)); - obj.setProperty("albedo", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.albedo)); + if (material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) { + obj.setProperty("opacity", FALLTHROUGH); + } else if (material.key.isTranslucentFactor()) { + obj.setProperty("opacity", material.opacity); + } - obj.setProperty("emissiveMap", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT) ? FALLTHROUGH : material.emissiveMap); - obj.setProperty("albedoMap", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT) ? FALLTHROUGH : material.albedoMap); - obj.setProperty("opacityMap", material.opacityMap); - obj.setProperty("occlusionMap", material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT) ? FALLTHROUGH : material.occlusionMap); - obj.setProperty("lightmapMap", material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT) ? FALLTHROUGH : material.lightmapMap); - obj.setProperty("scatteringMap", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT) ? FALLTHROUGH : material.scatteringMap); + if (material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) { + obj.setProperty("roughness", FALLTHROUGH); + } else if (material.key.isGlossy()) { + obj.setProperty("roughness", material.roughness); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) { + obj.setProperty("metallic", FALLTHROUGH); + } else if (material.key.isMetallic()) { + obj.setProperty("metallic", material.metallic); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) { + obj.setProperty("scattering", FALLTHROUGH); + } else if (material.key.isScattering()) { + obj.setProperty("scattering", material.scattering); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) { + obj.setProperty("unlit", FALLTHROUGH); + } else if (material.key.isUnlit()) { + obj.setProperty("unlit", material.unlit); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) { + obj.setProperty("emissive", FALLTHROUGH); + } else if (material.key.isEmissive()) { + obj.setProperty("emissive", vec3ColorToScriptValue(engine, material.emissive)); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) { + obj.setProperty("albedo", FALLTHROUGH); + } else if (material.key.isAlbedo()) { + obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo)); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) { + obj.setProperty("emissiveMap", FALLTHROUGH); + } else if (!material.emissiveMap.isEmpty()) { + obj.setProperty("emissiveMap", material.emissiveMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) { + obj.setProperty("albedoMap", FALLTHROUGH); + } else if (!material.albedoMap.isEmpty()) { + obj.setProperty("albedoMap", material.albedoMap); + } + + if (!material.opacityMap.isEmpty()) { + obj.setProperty("opacityMap", material.opacityMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) { + obj.setProperty("occlusionMap", FALLTHROUGH); + } else if (!material.occlusionMap.isEmpty()) { + obj.setProperty("occlusionMap", material.occlusionMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) { + obj.setProperty("lightmapMap", FALLTHROUGH); + } else if (!material.lightmapMap.isEmpty()) { + obj.setProperty("lightmapMap", material.lightmapMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) { + obj.setProperty("scatteringMap", FALLTHROUGH); + } else if (!material.scatteringMap.isEmpty()) { + obj.setProperty("scatteringMap", material.scatteringMap); + } // Only set one of each of these if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp index 4ff751782c..fdd06ffa64 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -45,75 +45,80 @@ scriptable::ScriptableMaterial& scriptable::ScriptableMaterial::operator=(const defaultFallthrough = material.defaultFallthrough; propertyFallthroughs = material.propertyFallthroughs; + key = material.key; + return *this; } -scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) : - name(material->getName().c_str()), - model(material->getModel().c_str()), - opacity(material->getOpacity()), - roughness(material->getRoughness()), - metallic(material->getMetallic()), - scattering(material->getScattering()), - unlit(material->isUnlit()), - emissive(material->getEmissive()), - albedo(material->getAlbedo()), - defaultFallthrough(material->getDefaultFallthrough()), - propertyFallthroughs(material->getPropertyFallthroughs()) -{ - auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP); - if (map && map->getTextureSource()) { - emissiveMap = map->getTextureSource()->getUrl().toString(); - } +scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) { + if (material) { + name = material->getName().c_str(); + model = material->getModel().c_str(); + opacity = material->getOpacity(); + roughness = material->getRoughness(); + metallic = material->getMetallic(); + scattering = material->getScattering(); + unlit = material->isUnlit(); + emissive = material->getEmissive(); + albedo = material->getAlbedo(); + defaultFallthrough = material->getDefaultFallthrough(); + propertyFallthroughs = material->getPropertyFallthroughs(); + key = material->getKey(); - map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP); - if (map && map->getTextureSource()) { - albedoMap = map->getTextureSource()->getUrl().toString(); - if (map->useAlphaChannel()) { - opacityMap = albedoMap; + auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP); + if (map && map->getTextureSource()) { + emissiveMap = map->getTextureSource()->getUrl().toString(); } - } - map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) { - metallicMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) { - specularMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP); + if (map && map->getTextureSource()) { + albedoMap = map->getTextureSource()->getUrl().toString(); + if (map->useAlphaChannel()) { + opacityMap = albedoMap; + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) { - roughnessMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) { - glossMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) { + metallicMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) { + specularMap = map->getTextureSource()->getUrl().toString(); + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP); - if (map && map->getTextureSource()) { - if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) { - normalMap = map->getTextureSource()->getUrl().toString(); - } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) { - bumpMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) { + roughnessMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) { + glossMap = map->getTextureSource()->getUrl().toString(); + } } - } - map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP); - if (map && map->getTextureSource()) { - occlusionMap = map->getTextureSource()->getUrl().toString(); - } + map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP); + if (map && map->getTextureSource()) { + if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) { + normalMap = map->getTextureSource()->getUrl().toString(); + } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) { + bumpMap = map->getTextureSource()->getUrl().toString(); + } + } - map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP); - if (map && map->getTextureSource()) { - lightmapMap = map->getTextureSource()->getUrl().toString(); - } + map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP); + if (map && map->getTextureSource()) { + occlusionMap = map->getTextureSource()->getUrl().toString(); + } - map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP); - if (map && map->getTextureSource()) { - scatteringMap = map->getTextureSource()->getUrl().toString(); + map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP); + if (map && map->getTextureSource()) { + lightmapMap = map->getTextureSource()->getUrl().toString(); + } + + map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP); + if (map && map->getTextureSource()) { + scatteringMap = map->getTextureSource()->getUrl().toString(); + } } } diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 022c9769fe..18ad37d7b9 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 fbx hfm baking graphics) +link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking) setup_memory_debugger() diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index f5fffe6ea3..1aae6ccb72 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -23,6 +23,7 @@ #include "baking/BakerLibrary.h" #include "JSBaker.h" #include "TextureBaker.h" +#include "MaterialBaker.h" BakerCLI::BakerCLI(OvenCLIApplication* parent) : QObject(parent) { @@ -60,8 +61,8 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else if (type == MATERIAL_EXTENSION) { - //_baker = std::unique_ptr { new MaterialBaker(inputUrl, outputPath) }; - //_baker->moveToThread(Oven::instance().getNextWorkerThread()); + _baker = std::unique_ptr { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath) }; + _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { // If the type doesn't match the above, we assume we have a texture, and the type specified is the // texture usage type (albedo, cubemap, normals, etc.) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 2eb2c8a36b..ca5c9b85fe 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -187,8 +187,8 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i // setup a texture baker for this URL, as long as we aren't baking a texture already if (!_textureBakers.contains(textureURL)) { - // setup a baker for this texture + // setup a baker for this texture QSharedPointer textureBaker { new TextureBaker(textureURL, type, _contentOutputPath), &TextureBaker::deleteLater @@ -220,16 +220,16 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ // grab a clean version of the URL without a query or fragment QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - // setup a texture baker for this URL, as long as we aren't baking a texture already + // setup a script baker for this URL, as long as we aren't baking a texture already if (!_scriptBakers.contains(scriptURL)) { - // setup a baker for this texture + // setup a baker for this script QSharedPointer scriptBaker { new JSBaker(scriptURL, _contentOutputPath), &JSBaker::deleteLater }; - // make sure our handler is called when the texture baker is done + // make sure our handler is called when the script baker is done connect(scriptBaker.data(), &JSBaker::finished, this, &DomainBaker::handleFinishedScriptBaker); // insert it into our bakers hash so we hold a strong pointer to it @@ -243,11 +243,48 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ ++_totalNumberOfSubBakes; } - // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // add this QJsonValueRef to our multi hash so that it can re-write the script URL // to the baked version once the baker is complete _entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef }); } +void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef) { + // grab a clean version of the URL without a query or fragment + QString materialData; + if (isURL) { + materialData = QUrl(data).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + } else { + materialData = data; + } + + // setup a material baker for this URL, as long as we aren't baking a material already + if (!_materialBakers.contains(materialData)) { + + // setup a baker for this material + QSharedPointer materialBaker { + new MaterialBaker(data, isURL, _contentOutputPath), + &MaterialBaker::deleteLater + }; + + // make sure our handler is called when the material baker is done + connect(materialBaker.data(), &MaterialBaker::finished, this, &DomainBaker::handleFinishedMaterialBaker); + + // insert it into our bakers hash so we hold a strong pointer to it + _materialBakers.insert(materialData, materialBaker); + + // move the baker to a worker thread and kickoff the bake + materialBaker->moveToThread(Oven::instance().getNextWorkerThread()); + QMetaObject::invokeMethod(materialBaker.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 texture URL + // to the baked version once the baker is complete + _entitiesNeedingRewrite.insert(materialData, { property, jsonRef }); +} + // All the Entity Properties that can be baked // *************************************************************************************** @@ -348,7 +385,12 @@ void DomainBaker::enumerateEntities() { } // Materials - // TODO + if (entity.contains(MATERIAL_URL_KEY)) { + addMaterialBaker(MATERIAL_URL_KEY, entity[MATERIAL_URL_KEY].toString(), true, *it); + } + if (entity.contains(MATERIAL_DATA_KEY)) { + addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_URL_KEY].toString(), false, *it); + } } } @@ -568,6 +610,91 @@ void DomainBaker::handleFinishedScriptBaker() { } } +void DomainBaker::handleFinishedMaterialBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this MaterialBaker is done and everything went according to plan + qDebug() << "Re-writing entity references to" << baker->getMaterialData(); + + QString newDataOrURL; + if (baker->isURL()) { + newDataOrURL = _destinationPath.resolved(baker->getBakedMaterialData()).toDisplayString(); + } else { + newDataOrURL = baker->getBakedMaterialData(); + } + + // enumerate the QJsonRef values for the URL of this material from our multi hash of + // entity objects needing a URL re-write + for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getMaterialData())) { + QString property = propertyEntityPair.first; + // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL + auto entity = propertyEntityPair.second.toObject(); + + if (!property.contains(".")) { + // grab the old URL + QUrl oldURL = entity[property].toString(); + + // copy the fragment and query, and user info from the old material data + if (baker->isURL()) { + QUrl newURL = newDataOrURL; + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + entity[property] = newURL.toString(); + } else { + entity[property] = newDataOrURL; + } + } else { + // Group property + QStringList propertySplit = property.split("."); + assert(propertySplit.length() == 2); + // grab the old URL + auto oldObject = entity[propertySplit[0]].toObject(); + QUrl oldURL = oldObject[propertySplit[1]].toString(); + + // copy the fragment and query, and user info from the old material data + if (baker->isURL()) { + QUrl newURL = newDataOrURL; + newURL.setQuery(oldURL.query()); + newURL.setFragment(oldURL.fragment()); + newURL.setUserInfo(oldURL.userInfo()); + + // set the new URL as the value in our temp QJsonObject + oldObject[propertySplit[1]] = newURL.toString(); + entity[propertySplit[0]] = oldObject; + } else { + oldObject[propertySplit[1]] = newDataOrURL; + entity[propertySplit[0]] = oldObject; + } + } + + // replace our temp object with the value referenced by our QJsonValueRef + propertyEntityPair.second = entity; + } + } else { + // this material failed to bake - this doesn't fail the entire bake but we need to add + // the errors from the material to our warnings + _warningList << baker->getErrors(); + } + + // remove the baked URL from the multi hash of entities needing a re-write + _entitiesNeedingRewrite.remove(baker->getMaterialData()); + + // drop our shared pointer to this baker so that it gets cleaned up + _materialBakers.remove(baker->getMaterialData()); + + // emit progress to tell listeners how many materials we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + + // check if this was the last material we needed to re-write and if we are done now + checkIfRewritingComplete(); + } +} + void DomainBaker::checkIfRewritingComplete() { if (_entitiesNeedingRewrite.isEmpty()) { writeNewEntitiesFile(); diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 2a9522143e..4504d5b8fa 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -21,6 +21,7 @@ #include "ModelBaker.h" #include "TextureBaker.h" #include "JSBaker.h" +#include "MaterialBaker.h" class DomainBaker : public Baker { Q_OBJECT @@ -40,6 +41,7 @@ private slots: void handleFinishedModelBaker(); void handleFinishedTextureBaker(); void handleFinishedScriptBaker(); + void handleFinishedMaterialBaker(); private: void setupOutputFolder(); @@ -62,6 +64,7 @@ private: QHash> _modelBakers; QHash> _textureBakers; QHash> _scriptBakers; + QHash> _materialBakers; QMultiHash> _entitiesNeedingRewrite; @@ -71,6 +74,7 @@ private: void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); + void addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef); }; #endif // hifi_DomainBaker_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index af98376034..6fdc45f6eb 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -63,6 +63,10 @@ void Oven::setupWorkerThreads(int numWorkerThreads) { } QThread* Oven::getNextWorkerThread() { + // FIXME: we assign these threads when we make the bakers, but if certain bakers finish quickly, we could end up + // in a situation where threads have finished and others have tons of work queued. Instead of assigning them at initialization, + // we should build a queue of bakers, and when threads finish, they can take the next available baker. + // 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 From 1a1277e9e70fd82b7fc2b66206601b08d9776ce5 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 27 Feb 2019 18:00:37 -0800 Subject: [PATCH 015/117] it's working! --- libraries/baking/CMakeLists.txt | 2 +- libraries/baking/src/MaterialBaker.cpp | 155 +++++++++++++++--- libraries/baking/src/MaterialBaker.h | 12 +- libraries/baking/src/TextureBaker.cpp | 11 +- libraries/baking/src/TextureBaker.h | 8 + .../GraphicsScriptingInterface.cpp | 40 ++--- .../GraphicsScriptingInterface.h | 4 + tools/oven/src/DomainBaker.cpp | 16 +- tools/oven/src/Oven.cpp | 10 ++ 9 files changed, 203 insertions(+), 55 deletions(-) diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index 2fa4c86691..38b6268fb7 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME baking) setup_hifi_library(Concurrent) -link_hifi_libraries(shared shaders graphics networking material-networking ktx image fbx) +link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 3427663b09..054d1ed0fd 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -19,10 +19,17 @@ #include #include +#include + +std::function MaterialBaker::_getNextOvenWorkerThreadOperator; + +static int materialNum = 0; + MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) : _materialData(materialData), _isURL(isURL), - _bakedOutputDir(bakedOutputDir) + _bakedOutputDir(bakedOutputDir), + _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)) { } @@ -51,7 +58,7 @@ void MaterialBaker::loadMaterial() { _materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource()); // TODO: add baseURL to allow these to reference relative files next to them - _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromVariant(_materialData), QUrl()); + _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(_materialData.toUtf8()), QUrl()); } else { qCDebug(material_baking) << "Downloading material" << _materialData; _materialResource = MaterialCache::instance().getMaterial(_materialData); @@ -71,47 +78,151 @@ void MaterialBaker::loadMaterial() { void MaterialBaker::processMaterial() { if (!_materialResource || _materialResource->parsedMaterials.networkMaterials.size() == 0) { handleError("Error processing " + _materialData); + return; } - _numTexturesToLoad = _materialResource->parsedMaterials.networkMaterials.size(); - _numTexturesLoaded = 0; + if (QDir(_textureOutputDir).exists()) { + qWarning() << "Output path" << _textureOutputDir << "already exists. Continuing."; + } else { + qCDebug(material_baking) << "Creating materialTextures output folder" << _textureOutputDir; + if (!QDir().mkpath(_textureOutputDir)) { + handleError("Failed to create materialTextures output folder " + _textureOutputDir); + } + } for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { if (networkMaterial.second) { auto textureMaps = networkMaterial.second->getTextureMaps(); for (auto textureMap : textureMaps) { if (textureMap.second && textureMap.second->getTextureSource()) { - auto texture = textureMap.second->getTextureSource(); graphics::Material::MapChannel mapChannel = textureMap.first; + auto texture = textureMap.second->getTextureSource(); - qDebug() << "boop" << mapChannel << texture->getUrl(); + QUrl url = texture->getUrl(); + QString cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); + auto idx = cleanURL.lastIndexOf('.'); + auto extension = idx >= 0 ? url.toDisplayString().mid(idx + 1).toLower() : ""; + + if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { + QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); + + // FIXME: this isn't properly handling bumpMaps or glossMaps + static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP { + { graphics::Material::MapChannel::EMISSIVE_MAP, image::TextureUsage::EMISSIVE_TEXTURE }, + { graphics::Material::MapChannel::ALBEDO_MAP, image::TextureUsage::ALBEDO_TEXTURE }, + { graphics::Material::MapChannel::METALLIC_MAP, image::TextureUsage::METALLIC_TEXTURE }, + { graphics::Material::MapChannel::ROUGHNESS_MAP, image::TextureUsage::ROUGHNESS_TEXTURE }, + { graphics::Material::MapChannel::NORMAL_MAP, image::TextureUsage::NORMAL_TEXTURE }, + { graphics::Material::MapChannel::OCCLUSION_MAP, image::TextureUsage::OCCLUSION_TEXTURE }, + { graphics::Material::MapChannel::LIGHTMAP_MAP, image::TextureUsage::LIGHTMAP_TEXTURE }, + { graphics::Material::MapChannel::SCATTERING_MAP, image::TextureUsage::SCATTERING_TEXTURE } + }; + + auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel); + if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) { + handleError("Unknown map channel"); + return; + } + + QPair textureKey = { textureURL, it->second }; + if (!_textureBakers.contains(textureKey)) { + QSharedPointer textureBaker { + new TextureBaker(textureURL, it->second, _textureOutputDir), + &TextureBaker::deleteLater + }; + textureBaker->setMapChannel(mapChannel); + connect(textureBaker.data(), &TextureBaker::finished, this, &MaterialBaker::handleFinishedTextureBaker); + _textureBakers.insert(textureKey, textureBaker); + textureBaker->moveToThread(_getNextOvenWorkerThreadOperator ? _getNextOvenWorkerThreadOperator() : thread()); + QMetaObject::invokeMethod(textureBaker.data(), "bake"); + } + _materialsNeedingRewrite.insert(textureKey, networkMaterial.second); + } else { + qCDebug(material_baking) << "Texture extension not supported: " << extension; + } } } } } + + if (_textureBakers.empty()) { + outputMaterial(); + } +} + +void MaterialBaker::handleFinishedTextureBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + QPair textureKey = { baker->getTextureURL(), baker->getTextureType() }; + if (!baker->hasErrors()) { + // this TextureBaker is done and everything went according to plan + qCDebug(material_baking) << "Re-writing texture references to" << baker->getTextureURL(); + + auto newURL = QUrl(_textureOutputDir).resolved(baker->getMetaTextureFileName()); + + // Replace the old texture URLs + for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) { + networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(newURL); + } + } else { + // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the texture to our warnings + _warningList << baker->getWarnings(); + } + + _materialsNeedingRewrite.remove(textureKey); + _textureBakers.remove(textureKey); + + if (_textureBakers.empty()) { + outputMaterial(); + } + } } void MaterialBaker::outputMaterial() { - //if (_isURL) { - // auto fileName = _materialData; - // auto baseName = fileName.left(fileName.lastIndexOf('.')); - // auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION; + if (_materialResource) { + QJsonDocument json; + if (_materialResource->parsedMaterials.networkMaterials.size() == 1) { + auto networkMaterial = _materialResource->parsedMaterials.networkMaterials.begin(); + auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial->second); + QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); + json = QJsonDocument::fromVariant(materialVariant); + } else { + QJsonArray materialArray; + for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { + auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial.second); + QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); + materialArray.append(QJsonDocument::fromVariant(materialVariant).object()); + } + json.setArray(materialArray); + } - // _bakedMaterialData = _bakedOutputDir + "/" + bakedFilename; + QByteArray outputMaterial = json.toJson(QJsonDocument::Compact); + if (_isURL) { + auto fileName = QUrl(_materialData).fileName(); + auto baseName = fileName.left(fileName.lastIndexOf('.')); + auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION; - // QFile bakedFile; - // bakedFile.setFileName(_bakedMaterialData); - // if (!bakedFile.open(QIODevice::WriteOnly)) { - // handleError("Error opening " + _bakedMaterialData + " for writing"); - // return; - // } + _bakedMaterialData = _bakedOutputDir + "/" + bakedFilename; - // bakedFile.write(outputMaterial); + QFile bakedFile; + bakedFile.setFileName(_bakedMaterialData); + if (!bakedFile.open(QIODevice::WriteOnly)) { + handleError("Error opening " + _bakedMaterialData + " for writing"); + return; + } - // // Export successful - // _outputFiles.push_back(_bakedMaterialData); - // qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData; - //} + bakedFile.write(outputMaterial); + + // Export successful + _outputFiles.push_back(_bakedMaterialData); + qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData; + } else { + _bakedMaterialData = QString(outputMaterial); + qCDebug(material_baking) << "Converted" << _materialData << "to" << _bakedMaterialData; + } + } // emit signal to indicate the material baking is finished emit finished(); diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index 6113515b81..b1678e5634 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -29,6 +29,8 @@ public: bool isURL() const { return _isURL; } QString getBakedMaterialData() const { return _bakedMaterialData; } + static void setNextOvenWorkerThreadOperator(std::function getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; } + public slots: virtual void bake() override; @@ -38,6 +40,7 @@ signals: private slots: void processMaterial(); void outputMaterial(); + void handleFinishedTextureBaker(); private: void loadMaterial(); @@ -46,13 +49,16 @@ private: bool _isURL; NetworkMaterialResourcePointer _materialResource; - size_t _numTexturesToLoad { 0 }; - size_t _numTexturesLoaded { 0 }; - QHash> _textureBakers; + QHash, QSharedPointer> _textureBakers; + QMultiHash, std::shared_ptr> _materialsNeedingRewrite; QString _bakedOutputDir; + QString _textureOutputDir; QString _bakedMaterialData; + + QScriptEngine _scriptEngine; + static std::function _getNextOvenWorkerThreadOperator; }; #endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 6407ce1846..db54cbdf98 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -128,7 +128,14 @@ void TextureBaker::processTexture() { TextureMeta meta; - auto originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName()); + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); + _baseFilename += addMapChannel; + + QString newFilename = _textureURL.fileName(); + newFilename.replace(QString("."), addMapChannel + "."); + QString originalCopyFilePath = _outputDirectory.absoluteFilePath(newFilename); + { QFile file { originalCopyFilePath }; if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) { @@ -138,7 +145,7 @@ void TextureBaker::processTexture() { // IMPORTANT: _originalTexture is empty past this point _originalTexture.clear(); _outputFiles.push_back(originalCopyFilePath); - meta.original = _metaTexturePathPrefix + _textureURL.fileName(); + meta.original = _metaTexturePathPrefix + newFilename; } auto buffer = std::static_pointer_cast(std::make_shared(originalCopyFilePath)); diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index c8c4fb73b8..84e7c57aa1 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -22,6 +22,8 @@ #include "Baker.h" +#include + extern const QString BAKED_TEXTURE_KTX_EXT; extern const QString BAKED_META_TEXTURE_SUFFIX; @@ -43,6 +45,10 @@ public: static void setCompressionEnabled(bool enabled) { _compressionEnabled = enabled; } + void setMapChannel(graphics::Material::MapChannel mapChannel) { _mapChannel = mapChannel; } + graphics::Material::MapChannel getMapChannel() const { return _mapChannel; } + image::TextureUsage::Type getTextureType() const { return _textureType; } + public slots: virtual void bake() override; virtual void abort() override; @@ -60,6 +66,8 @@ private: QUrl _textureURL; QByteArray _originalTexture; image::TextureUsage::Type _textureType; + graphics::Material::MapChannel _mapChannel; + bool _mapChannelSet { false }; QString _baseFilename; QDir _outputDirectory; diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 1fd7ad9df5..3bd4af601c 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -363,56 +363,58 @@ namespace scriptable { obj.setProperty("name", material.name); obj.setProperty("model", material.model); + bool hasPropertyFallthroughs = !material.propertyFallthroughs.empty(); + const QScriptValue FALLTHROUGH("fallthrough"); - if (material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) { obj.setProperty("opacity", FALLTHROUGH); } else if (material.key.isTranslucentFactor()) { obj.setProperty("opacity", material.opacity); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) { obj.setProperty("roughness", FALLTHROUGH); } else if (material.key.isGlossy()) { obj.setProperty("roughness", material.roughness); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) { obj.setProperty("metallic", FALLTHROUGH); } else if (material.key.isMetallic()) { obj.setProperty("metallic", material.metallic); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) { obj.setProperty("scattering", FALLTHROUGH); } else if (material.key.isScattering()) { obj.setProperty("scattering", material.scattering); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) { obj.setProperty("unlit", FALLTHROUGH); } else if (material.key.isUnlit()) { obj.setProperty("unlit", material.unlit); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) { obj.setProperty("emissive", FALLTHROUGH); } else if (material.key.isEmissive()) { obj.setProperty("emissive", vec3ColorToScriptValue(engine, material.emissive)); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) { obj.setProperty("albedo", FALLTHROUGH); } else if (material.key.isAlbedo()) { obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo)); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) { obj.setProperty("emissiveMap", FALLTHROUGH); } else if (!material.emissiveMap.isEmpty()) { obj.setProperty("emissiveMap", material.emissiveMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) { obj.setProperty("albedoMap", FALLTHROUGH); } else if (!material.albedoMap.isEmpty()) { obj.setProperty("albedoMap", material.albedoMap); @@ -422,26 +424,26 @@ namespace scriptable { obj.setProperty("opacityMap", material.opacityMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) { obj.setProperty("occlusionMap", FALLTHROUGH); } else if (!material.occlusionMap.isEmpty()) { obj.setProperty("occlusionMap", material.occlusionMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) { obj.setProperty("lightmapMap", FALLTHROUGH); } else if (!material.lightmapMap.isEmpty()) { obj.setProperty("lightmapMap", material.lightmapMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) { obj.setProperty("scatteringMap", FALLTHROUGH); } else if (!material.scatteringMap.isEmpty()) { obj.setProperty("scatteringMap", material.scatteringMap); } // Only set one of each of these - if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { obj.setProperty("metallicMap", FALLTHROUGH); } else if (!material.metallicMap.isEmpty()) { obj.setProperty("metallicMap", material.metallicMap); @@ -449,7 +451,7 @@ namespace scriptable { obj.setProperty("specularMap", material.specularMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) { obj.setProperty("roughnessMap", FALLTHROUGH); } else if (!material.roughnessMap.isEmpty()) { obj.setProperty("roughnessMap", material.roughnessMap); @@ -457,7 +459,7 @@ namespace scriptable { obj.setProperty("glossMap", material.glossMap); } - if (material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) { obj.setProperty("normalMap", FALLTHROUGH); } else if (!material.normalMap.isEmpty()) { obj.setProperty("normalMap", material.normalMap); @@ -466,16 +468,16 @@ namespace scriptable { } // These need to be implemented, but set the fallthrough for now - if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { obj.setProperty("texCoordTransform0", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { obj.setProperty("texCoordTransform1", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { obj.setProperty("lightmapParams", FALLTHROUGH); } - if (material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) { + if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) { obj.setProperty("materialParams", FALLTHROUGH); } diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h index a72c3be14b..267ba01041 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h @@ -103,6 +103,10 @@ private: }; +namespace scriptable { + QScriptValue scriptableMaterialToScriptValue(QScriptEngine* engine, const scriptable::ScriptableMaterial &material); +}; + Q_DECLARE_METATYPE(glm::uint32) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(NestableType) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index ca5c9b85fe..a74e402b63 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -220,7 +220,7 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ // grab a clean version of the URL without a query or fragment QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); - // setup a script baker for this URL, as long as we aren't baking a texture already + // setup a script baker for this URL, as long as we aren't baking a script already if (!_scriptBakers.contains(scriptURL)) { // setup a baker for this script @@ -255,7 +255,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, materialData = QUrl(data).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); } else { materialData = data; - } + } // setup a material baker for this URL, as long as we aren't baking a material already if (!_materialBakers.contains(materialData)) { @@ -280,7 +280,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, ++_totalNumberOfSubBakes; } - // add this QJsonValueRef to our multi hash so that it can re-write the texture URL + // add this QJsonValueRef to our multi hash so that it can re-write the material URL // to the baked version once the baker is complete _entitiesNeedingRewrite.insert(materialData, { property, jsonRef }); } @@ -389,7 +389,7 @@ void DomainBaker::enumerateEntities() { addMaterialBaker(MATERIAL_URL_KEY, entity[MATERIAL_URL_KEY].toString(), true, *it); } if (entity.contains(MATERIAL_DATA_KEY)) { - addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_URL_KEY].toString(), false, *it); + addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_DATA_KEY].toString(), false, *it); } } } @@ -533,11 +533,11 @@ void DomainBaker::handleFinishedTextureBaker() { // drop our shared pointer to this baker so that it gets cleaned up _textureBakers.remove(baker->getTextureURL()); - // emit progress to tell listeners how many textures we have baked - emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); + // emit progress to tell listeners how many textures we have baked + emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); - // check if this was the last texture we needed to re-write and if we are done now - checkIfRewritingComplete(); + // check if this was the last texture we needed to re-write and if we are done now + checkIfRewritingComplete(); } } diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 6fdc45f6eb..c70ca27d8b 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -20,6 +20,10 @@ #include #include #include +#include +#include + +#include "MaterialBaker.h" Oven* Oven::_staticInstance { nullptr }; @@ -33,6 +37,12 @@ Oven::Oven() { DependencyManager::set(); DependencyManager::set(false); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + MaterialBaker::setNextOvenWorkerThreadOperator([] { + return Oven::instance().getNextWorkerThread(); + }); } Oven::~Oven() { From 168e47aa62fb790c4ea87aeaa2d1f5fdf0a4e518 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 28 Feb 2019 09:59:56 -0800 Subject: [PATCH 016/117] bake particles and polylines --- libraries/baking/src/MaterialBaker.cpp | 8 ++++---- tools/oven/src/DomainBaker.cpp | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 054d1ed0fd..558adedf68 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -182,12 +182,12 @@ void MaterialBaker::handleFinishedTextureBaker() { void MaterialBaker::outputMaterial() { if (_materialResource) { - QJsonDocument json; + QJsonObject json; if (_materialResource->parsedMaterials.networkMaterials.size() == 1) { auto networkMaterial = _materialResource->parsedMaterials.networkMaterials.begin(); auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial->second); QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); - json = QJsonDocument::fromVariant(materialVariant); + json.insert("materials", QJsonDocument::fromVariant(materialVariant).object()); } else { QJsonArray materialArray; for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) { @@ -195,10 +195,10 @@ void MaterialBaker::outputMaterial() { QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant(); materialArray.append(QJsonDocument::fromVariant(materialVariant).object()); } - json.setArray(materialArray); + json.insert("materials", materialArray); } - QByteArray outputMaterial = json.toJson(QJsonDocument::Compact); + QByteArray outputMaterial = QJsonDocument(json).toJson(QJsonDocument::Compact); if (_isURL) { auto fileName = QUrl(_materialData).fileName(); auto baseName = fileName.left(fileName.lastIndexOf('.')); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index a74e402b63..42dfe59241 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -288,6 +288,8 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, // All the Entity Properties that can be baked // *************************************************************************************** +const QString TYPE_KEY = "type"; + // Models const QString MODEL_URL_KEY = "modelURL"; const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; @@ -349,7 +351,13 @@ void DomainBaker::enumerateEntities() { // Textures if (entity.contains(TEXTURES_KEY)) { - // TODO: the textures property is treated differently for different entity types + if (entity.contains(TYPE_KEY)) { + QString type = entity[TYPE_KEY].toString(); + // TODO: handle textures for model entities + if (type == "ParticleEffect" || type == "PolyLine") { + addTextureBaker(TEXTURES_KEY, entity[TEXTURES_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); + } + } } if (entity.contains(IMAGE_URL_KEY)) { addTextureBaker(IMAGE_URL_KEY, entity[IMAGE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it); From 82382fe9a1eac467495c5fd1e7e5b785ba41f425 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 16:10:40 -0800 Subject: [PATCH 017/117] Use hifi types consistently in model-baker --- libraries/model-baker/src/model-baker/Baker.cpp | 6 ++---- libraries/model-baker/src/model-baker/Baker.h | 6 ++---- libraries/model-baker/src/model-baker/PrepareJointsTask.cpp | 4 ++-- libraries/model-baker/src/model-baker/PrepareJointsTask.h | 5 ++--- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index fc27756877..4d740f4a94 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -11,8 +11,6 @@ #include "Baker.h" -#include - #include "BakerTypes.h" #include "ModelMath.h" #include "BuildGraphicsMeshTask.h" @@ -118,7 +116,7 @@ namespace baker { class BakerEngineBuilder { public: - using Input = VaryingSet2; + using Input = VaryingSet2; using Output = VaryingSet2; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { @@ -170,7 +168,7 @@ namespace baker { } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { _engine->feedInput(0, hfmModel); _engine->feedInput(1, mapping); diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 1880aba618..e8a97b863d 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -12,8 +12,7 @@ #ifndef hifi_baker_Baker_h #define hifi_baker_Baker_h -#include - +#include #include #include "Engine.h" @@ -23,7 +22,7 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping); + Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping); void run(); @@ -34,7 +33,6 @@ namespace baker { protected: EnginePointer _engine; }; - }; #endif //hifi_baker_Baker_h diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 63d0408337..e5a2079d3f 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -13,7 +13,7 @@ #include "ModelBakerLogging.h" -QMap getJointNameMapping(const QVariantHash& mapping) { +QMap getJointNameMapping(const hifi::VariantHash& mapping) { static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; QMap hfmToHifiJointNameMap; if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { @@ -26,7 +26,7 @@ QMap getJointNameMapping(const QVariantHash& mapping) { return hfmToHifiJointNameMap; } -QMap getJointRotationOffsets(const QVariantHash& mapping) { +QMap getJointRotationOffsets(const hifi::VariantHash& mapping) { QMap jointRotationOffsets; static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) { diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index 6185d2fdad..0dbb9d584d 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -12,8 +12,7 @@ #ifndef hifi_PrepareJointsTask_h #define hifi_PrepareJointsTask_h -#include - +#include #include #include "Engine.h" @@ -29,7 +28,7 @@ public: class PrepareJointsTask { public: using Config = PrepareJointsTaskConfig; - using Input = baker::VaryingSet2, QVariantHash /*mapping*/>; + using Input = baker::VaryingSet2, hifi::VariantHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; using JobModel = baker::Job::ModelIO; From 1576125c4271839d6f88c025ee5cd8fd9fefb747 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 21 Feb 2019 15:36:31 -0800 Subject: [PATCH 018/117] Integrate HFM Asset Engine (aka model prep step) into Oven Add 'deduplicateIndices' parameter to FBXSerializer and make deduplicate a required parameter for extractMesh Add draco mesh and FBX draco node version Support generating/saving draco meshes from FBX Model nodes --- libraries/baking/CMakeLists.txt | 2 +- libraries/baking/src/FBXBaker.cpp | 272 +++++-------- libraries/baking/src/FBXBaker.h | 19 +- libraries/baking/src/ModelBaker.cpp | 371 ++++++++++-------- libraries/baking/src/ModelBaker.h | 12 +- libraries/baking/src/OBJBaker.cpp | 206 +++------- libraries/baking/src/OBJBaker.h | 16 +- libraries/baking/src/baking/BakerLibrary.cpp | 2 +- libraries/fbx/src/FBX.h | 3 + libraries/fbx/src/FBXSerializer.cpp | 14 +- libraries/fbx/src/FBXSerializer.h | 2 +- libraries/fbx/src/FBXSerializer_Mesh.cpp | 30 +- libraries/hfm/src/hfm/HFM.h | 3 + .../model-baker/src/model-baker/Baker.cpp | 25 +- libraries/model-baker/src/model-baker/Baker.h | 5 + .../src/model-baker/BuildDracoMeshTask.cpp | 233 +++++++++++ .../src/model-baker/BuildDracoMeshTask.h | 39 ++ .../src/model-baker/PrepareJointsTask.h | 4 +- .../src/model-networking/ModelCache.cpp | 1 + tools/oven/CMakeLists.txt | 2 +- tools/oven/src/Oven.cpp | 9 + tools/vhacd-util/src/VHACDUtil.cpp | 6 +- 22 files changed, 744 insertions(+), 532 deletions(-) create mode 100644 libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp create mode 100644 libraries/model-baker/src/model-baker/BuildDracoMeshTask.h diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index 38b6268fb7..aeb4346f93 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME baking) setup_hifi_library(Concurrent) -link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx) +link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx model-baker task) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 7c4354a2b6..e1bb86d051 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -37,24 +37,9 @@ #include "FBXToJSON.h" #endif -void FBXBaker::bake() { - qDebug() << "FBXBaker" << _modelURL << "bake starting"; - - // Setup the output folders for the results of this bake - initializeOutputDirs(); - - if (shouldStop()) { - 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 +void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { + _hfmModel = hfmModel; + // Load the root node from the FBX file importScene(); if (shouldStop()) { @@ -68,94 +53,7 @@ void FBXBaker::bakeSourceCopy() { return; } - rewriteAndBakeSceneModels(); - - if (shouldStop()) { - return; - } - - // check if we're already done with textures (in case we had none to re-write) - checkIfTexturesFinished(); -} - -void FBXBaker::loadSourceFBX() { - // check if the FBX is local or first needs to be downloaded - if (_modelURL.isLocalFile()) { - // load up the local file - QFile localFBX { _modelURL.toLocalFile() }; - - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; - - if (!localFBX.exists()) { - //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); - handleError("Could not find " + _modelURL.toString()); - return; - } - - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localFBX.copy(_originalModelFilePath); - - // 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(_modelURL); - - qCDebug(model_baking) << "Downloading" << _modelURL; - 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" << _modelURL; - - // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); - - qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << 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 " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); - return; - } - if (copyOfOriginal.write(requestReply->readAll()) == -1) { - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); - return; - } - - // close that file now that we are done writing to it - copyOfOriginal.close(); - - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.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 " + _modelURL.toString()); - } + rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists); } void FBXBaker::importScene() { @@ -167,10 +65,8 @@ void FBXBaker::importScene() { return; } - FBXSerializer fbxSerializer; - qCDebug(model_baking) << "Parsing" << _modelURL; - _rootNode = fbxSerializer._rootNode = fbxSerializer.parseFBX(&fbxFile); + _rootNode = FBXSerializer().parseFBX(&fbxFile); #ifdef HIFI_DUMP_FBX { @@ -185,85 +81,113 @@ void FBXBaker::importScene() { } } #endif - - _hfmModel = fbxSerializer.extractHFMModel({}, _modelURL.toString()); - _textureContentMap = fbxSerializer._textureContent; } -void FBXBaker::rewriteAndBakeSceneModels() { - unsigned int meshIndex = 0; - bool hasDeformers { false }; - for (FBXNode& rootChild : _rootNode.children) { - if (rootChild.name == "Objects") { - for (FBXNode& objectChild : rootChild.children) { - if (objectChild.name == "Deformer") { - hasDeformers = true; - break; - } +void FBXBaker::replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { + // Compress mesh information and store in dracoMeshNode + FBXNode dracoMeshNode; + bool success = buildDracoMeshNode(dracoMeshNode, dracoMeshBytes, dracoMaterialList); + + if (!success) { + return; + } else { + meshNode.children.push_back(dracoMeshNode); + + static const std::vector nodeNamesToDelete { + // Node data that is packed into the draco mesh + "Vertices", + "PolygonVertexIndex", + "LayerElementNormal", + "LayerElementColor", + "LayerElementUV", + "LayerElementMaterial", + "LayerElementTexture", + + // Node data that we don't support + "Edges", + "LayerElementTangent", + "LayerElementBinormal", + "LayerElementSmoothing" + }; + auto& children = meshNode.children; + auto it = children.begin(); + while (it != children.end()) { + auto begin = nodeNamesToDelete.begin(); + auto end = nodeNamesToDelete.end(); + if (find(begin, end, it->name) != end) { + it = children.erase(it); + } else { + ++it; } } - if (hasDeformers) { - break; - } } +} + +void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { + std::vector meshIndexToRuntimeOrder; + auto meshCount = (int)meshes.size(); + meshIndexToRuntimeOrder.resize(meshCount); + for (int i = 0; i < meshCount; i++) { + meshIndexToRuntimeOrder[meshes[i].meshIndex] = i; + } + + // The meshIndex represents the order in which the meshes are loaded from the FBX file + // We replicate this order by iterating over the meshes in the same way that FBXSerializer does + int meshIndex = 0; for (FBXNode& rootChild : _rootNode.children) { if (rootChild.name == "Objects") { - for (FBXNode& objectChild : rootChild.children) { - if (objectChild.name == "Geometry") { + for (FBXNode& object : rootChild.children) { + if (object.name == "Geometry") { + if (object.properties.at(2) == "Mesh") { + int meshNum = meshIndexToRuntimeOrder[meshIndex]; + replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + meshIndex++; + } + } else if (object.name == "Model") { + for (FBXNode& modelChild : object.children) { + bool properties = false; + hifi::ByteArray propertyName; + int index; + if (modelChild.name == "Properties60") { + properties = true; + propertyName = "Property"; + index = 3; - // TODO Pull this out of _hfmModel instead so we don't have to reprocess it - auto extractedMesh = FBXSerializer::extractMesh(objectChild, meshIndex, false); - - // Callback to get MaterialID - GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) { - return extractedMesh.partMaterialTextures[partIndex].first; - }; - - // Compress mesh information and store in dracoMeshNode - FBXNode dracoMeshNode; - bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback); - - // if bake fails - return, if there were errors and continue, if there were warnings. - if (!success) { - if (hasErrors()) { - return; - } else if (hasWarnings()) { - continue; + } else if (modelChild.name == "Properties70") { + properties = true; + propertyName = "P"; + index = 4; } - } else { - objectChild.children.push_back(dracoMeshNode); - static const std::vector nodeNamesToDelete { - // Node data that is packed into the draco mesh - "Vertices", - "PolygonVertexIndex", - "LayerElementNormal", - "LayerElementColor", - "LayerElementUV", - "LayerElementMaterial", - "LayerElementTexture", - - // Node data that we don't support - "Edges", - "LayerElementTangent", - "LayerElementBinormal", - "LayerElementSmoothing" - }; - auto& children = objectChild.children; - auto it = children.begin(); - while (it != children.end()) { - auto begin = nodeNamesToDelete.begin(); - auto end = nodeNamesToDelete.end(); - if (find(begin, end, it->name) != end) { - it = children.erase(it); - } else { - ++it; + if (properties) { + // This is a properties node + // Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer + static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); + static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation"); + static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling"); + for (int i = 0; i < modelChild.children.size(); i++) { + const auto& prop = modelChild.children[i]; + const auto& propertyName = prop.properties.at(0); + if (propertyName == GEOMETRIC_TRANSLATION || + propertyName == GEOMETRIC_ROTATION || + propertyName == GEOMETRIC_SCALING) { + modelChild.children.removeAt(i); + --i; + } } + } else if (modelChild.name == "Vertices") { + // This model is also a mesh + int meshNum = meshIndexToRuntimeOrder[meshIndex]; + replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]); + meshIndex++; } } - } // Geometry Object + } - } // foreach root child + if (hasErrors()) { + return; + } + } } } } diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 88443de1c0..7770e3014d 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -33,25 +33,16 @@ class FBXBaker : public ModelBaker { public: using ModelBaker::ModelBaker; -public slots: - virtual void bake() override; - -signals: - void sourceCopyReadyToLoad(); - -private slots: - void bakeSourceCopy(); - void handleFBXNetworkReply(); +protected: + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void loadSourceFBX(); - void importScene(); - void embedTextureMetaData(); - void rewriteAndBakeSceneModels(); + void rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists); void rewriteAndBakeSceneTextures(); + void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); - HFMModel* _hfmModel; + hfm::Model::Pointer _hfmModel; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 61eed9f655..6568850c1f 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -12,6 +12,13 @@ #include "ModelBaker.h" #include +#include + +#include +#include + +#include +#include #include @@ -31,6 +38,8 @@ #pragma warning( pop ) #endif +#include "baking/BakerLibrary.h" + ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : _modelURL(inputModelURL), @@ -65,6 +74,22 @@ ModelBaker::~ModelBaker() { } } +void ModelBaker::bake() { + qDebug() << "ModelBaker" << _modelURL << "bake starting"; + + // Setup the output folders for the results of this bake + initializeOutputDirs(); + + if (shouldStop()) { + return; + } + + connect(this, &ModelBaker::modelLoaded, this, &ModelBaker::bakeSourceCopy); + + // make a local copy of the model + saveSourceModel(); +} + void ModelBaker::initializeOutputDirs() { // Attempt to make the output folders // Warn if there is an output directory using the same name @@ -88,6 +113,166 @@ void ModelBaker::initializeOutputDirs() { } } +void ModelBaker::saveSourceModel() { + // check if the FBX is local or first needs to be downloaded + if (_modelURL.isLocalFile()) { + // load up the local file + QFile localModelURL { _modelURL.toLocalFile() }; + + qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; + + if (!localModelURL.exists()) { + //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); + handleError("Could not find " + _modelURL.toString()); + return; + } + + // make a copy in the output folder + if (!_originalOutputDir.isEmpty()) { + qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); + localModelURL.copy(_originalOutputDir + "/" + _modelURL.fileName()); + } + + localModelURL.copy(_originalModelFilePath); + + // emit our signal to start the import of the FBX source copy + emit modelLoaded(); + } 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(_modelURL); + + qCDebug(model_baking) << "Downloading" << _modelURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &ModelBaker::handleModelNetworkReply); + } +} + +void ModelBaker::handleModelNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _modelURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(_originalModelFilePath); + + qDebug(model_baking) << "Writing copy of original model file to" << _originalModelFilePath << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly)) { + // add an error to the error list for this model stating that a duplicate of the original model could not be made + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); + return; + } + if (copyOfOriginal.write(requestReply->readAll()) == -1) { + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + if (!_originalOutputDir.isEmpty()) { + copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); + } + + // emit our signal to start the import of the model source copy + emit modelLoaded(); + } else { + // add an error to our list stating that the model could not be downloaded + handleError("Failed to download " + _modelURL.toString()); + } +} + +// TODO: Remove after testing +#include + +void ModelBaker::bakeSourceCopy() { + QFile modelFile(_originalModelFilePath); + if (!modelFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalModelFilePath + " for reading"); + return; + } + hifi::ByteArray modelData = modelFile.readAll(); + + hfm::Model::Pointer bakedModel; + std::vector dracoMeshes; + std::vector> dracoMaterialLists; // Material order for per-mesh material lookup used by dracoMeshes + + { + auto serializer = DependencyManager::get()->getSerializerForMediaType(modelData, _modelURL, ""); + if (!serializer) { + handleError("Could not recognize file type of model file " + _originalModelFilePath); + return; + } + hifi::VariantHash mapping; + mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library + hfm::Model::Pointer loadedModel = serializer->read(modelData, mapping, _modelURL); + + baker::Baker baker(loadedModel, mapping); + auto config = baker.getConfiguration(); + // Enable compressed draco mesh generation + config->getJobConfig("BuildDracoMesh")->setEnabled(true); + // Do not permit potentially lossy modification of joint data meant for runtime + ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; + + // TODO: Remove after testing + { + auto* dracoConfig = ((BuildDracoMeshConfig*)config->getJobConfig("BuildDracoMesh")); + dracoConfig->encodeSpeed = 10; + dracoConfig->decodeSpeed = -1; + } + + // Begin hfm baking + baker.run(); + + bakedModel = baker.getHFMModel(); + dracoMeshes = baker.getDracoMeshes(); + dracoMaterialLists = baker.getDracoMaterialLists(); + } + + // Populate _textureContentMap with path to content mappings, for quick lookup by URL + for (auto materialIt = bakedModel->materials.cbegin(); materialIt != bakedModel->materials.cend(); materialIt++) { + static const auto addTexture = [](QHash& textureContentMap, const hfm::Texture& texture) { + if (!textureContentMap.contains(texture.filename)) { + // Content may be empty, unless the data is inlined + textureContentMap[texture.filename] = texture.content; + } + }; + const hfm::Material& material = *materialIt; + addTexture(_textureContentMap, material.normalTexture); + addTexture(_textureContentMap, material.albedoTexture); + addTexture(_textureContentMap, material.opacityTexture); + addTexture(_textureContentMap, material.glossTexture); + addTexture(_textureContentMap, material.roughnessTexture); + addTexture(_textureContentMap, material.specularTexture); + addTexture(_textureContentMap, material.metallicTexture); + addTexture(_textureContentMap, material.emissiveTexture); + addTexture(_textureContentMap, material.occlusionTexture); + addTexture(_textureContentMap, material.scatteringTexture); + addTexture(_textureContentMap, material.lightmapTexture); + } + + // Do format-specific baking + bakeProcessedSource(bakedModel, dracoMeshes, dracoMaterialLists); + + if (shouldStop()) { + return; + } + + // check if we're already done with textures (in case we had none to re-write) + checkIfTexturesFinished(); +} + void ModelBaker::abort() { Baker::abort(); @@ -98,176 +283,36 @@ void ModelBaker::abort() { } } -bool ModelBaker::compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { - if (mesh.wasCompressed) { - handleError("Cannot re-bake a file that contains compressed mesh"); +bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { + if (dracoMeshBytes.isEmpty()) { + handleError("Failed to finalize the baking of a draco Geometry node"); return false; } - Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size()); - Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); - Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); - - int64_t numTriangles{ 0 }; - for (auto& part : mesh.parts) { - if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) { - handleWarning("Found a mesh part with invalid index data, skipping"); - continue; - } - numTriangles += part.quadTrianglesIndices.size() / 3; - numTriangles += part.triangleIndices.size() / 3; - } - - if (numTriangles == 0) { - return false; - } - - draco::TriangleSoupMeshBuilder meshBuilder; - - meshBuilder.Start(numTriangles); - - bool hasNormals{ mesh.normals.size() > 0 }; - bool hasColors{ mesh.colors.size() > 0 }; - bool hasTexCoords{ mesh.texCoords.size() > 0 }; - bool hasTexCoords1{ mesh.texCoords1.size() > 0 }; - bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true; - bool needsOriginalIndices{ hasDeformers }; - - int normalsAttributeID { -1 }; - int colorsAttributeID { -1 }; - int texCoordsAttributeID { -1 }; - int texCoords1AttributeID { -1 }; - int faceMaterialAttributeID { -1 }; - int originalIndexAttributeID { -1 }; - - const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, - 3, draco::DT_FLOAT32); - if (needsOriginalIndices) { - originalIndexAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, - 1, draco::DT_INT32); - } - - if (hasNormals) { - normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, - 3, draco::DT_FLOAT32); - } - if (hasColors) { - colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, - 3, draco::DT_FLOAT32); - } - if (hasTexCoords) { - texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, - 2, draco::DT_FLOAT32); - } - if (hasTexCoords1) { - texCoords1AttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, - 2, draco::DT_FLOAT32); - } - if (hasPerFaceMaterials) { - faceMaterialAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, - 1, draco::DT_UINT16); - } - - auto partIndex = 0; - draco::FaceIndex face; - uint16_t materialID; - - for (auto& part : mesh.parts) { - materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex; - - auto addFace = [&](QVector& indices, int index, draco::FaceIndex face) { - int32_t idx0 = indices[index]; - int32_t idx1 = indices[index + 1]; - int32_t idx2 = indices[index + 2]; - - if (hasPerFaceMaterials) { - meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); - } - - meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, - &mesh.vertices[idx0], &mesh.vertices[idx1], - &mesh.vertices[idx2]); - - if (needsOriginalIndices) { - meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, - &mesh.originalIndices[idx0], - &mesh.originalIndices[idx1], - &mesh.originalIndices[idx2]); - } - if (hasNormals) { - meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, - &mesh.normals[idx0], &mesh.normals[idx1], - &mesh.normals[idx2]); - } - if (hasColors) { - meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, - &mesh.colors[idx0], &mesh.colors[idx1], - &mesh.colors[idx2]); - } - if (hasTexCoords) { - meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, - &mesh.texCoords[idx0], &mesh.texCoords[idx1], - &mesh.texCoords[idx2]); - } - if (hasTexCoords1) { - meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, - &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], - &mesh.texCoords1[idx2]); - } - }; - - for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { - addFace(part.quadTrianglesIndices, i, face++); - } - - for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { - addFace(part.triangleIndices, i, face++); - } - - partIndex++; - } - - auto dracoMesh = meshBuilder.Finalize(); - - if (!dracoMesh) { - handleWarning("Failed to finalize the baking of a draco Geometry node"); - return false; - } - - // we need to modify unique attribute IDs for custom attributes - // so the attributes are easily retrievable on the other side - if (hasPerFaceMaterials) { - dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); - } - - if (hasTexCoords1) { - dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); - } - - if (needsOriginalIndices) { - dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); - } - - draco::Encoder encoder; - - encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); - encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); - encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); - encoder.SetSpeedOptions(0, 5); - - draco::EncoderBuffer buffer; - encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); - FBXNode dracoNode; dracoNode.name = "DracoMesh"; - auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size())); - dracoNode.properties.append(value); + dracoNode.properties.append(QVariant::fromValue(dracoMeshBytes)); + // Additional draco mesh node information + { + FBXNode fbxVersionNode; + fbxVersionNode.name = "FBXDracoMeshVersion"; + fbxVersionNode.properties.append(FBX_DRACO_MESH_VERSION); + dracoNode.children.append(fbxVersionNode); + + FBXNode dracoVersionNode; + dracoVersionNode.name = "DracoMeshVersion"; + dracoVersionNode.properties.append(DRACO_MESH_VERSION); + dracoNode.children.append(dracoVersionNode); + + FBXNode materialListNode; + materialListNode.name = "MaterialList"; + for (const hifi::ByteArray& materialID : dracoMaterialList) { + materialListNode.properties.append(materialID); + } + dracoNode.children.append(materialListNode); + } dracoMeshNode = dracoNode; - // Mesh compression successful return true return true; } diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 0f0cfbe07c..b0bd3798ff 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -47,17 +47,23 @@ public: void initializeOutputDirs(); - bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); + bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); virtual void setWasAborted(bool wasAborted) override; QUrl getModelURL() const { return _modelURL; } QString getBakedModelFilePath() const { return _bakedModelFilePath; } +signals: + void modelLoaded(); + public slots: + virtual void bake() override; virtual void abort() override; protected: + void saveSourceModel(); + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) = 0; void checkIfTexturesFinished(); void texturesFinished(); void embedTextureMetaData(); @@ -72,6 +78,10 @@ protected: QDir _modelTempDir; QString _originalModelFilePath; +protected slots: + void handleModelNetworkReply(); + virtual void bakeSourceCopy(); + private slots: void handleBakedTexture(); void handleAbortedTexture(); diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index 11cac0b4c2..ebc24201f4 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -35,150 +35,51 @@ const QByteArray CONNECTIONS_NODE_PROPERTY = "OO"; const QByteArray CONNECTIONS_NODE_PROPERTY_1 = "OP"; const QByteArray MESH = "Mesh"; -void OBJBaker::bake() { - qDebug() << "OBJBaker" << _modelURL << "bake starting"; - - // Setup the output folders for the results of this bake - initializeOutputDirs(); - - // trigger bakeOBJ once OBJ is loaded - connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ); - - // make a local copy of the OBJ - loadOBJ(); -} - -void OBJBaker::loadOBJ() { - // check if the OBJ is local or it needs to be downloaded - if (_modelURL.isLocalFile()) { - // loading the local OBJ - QFile localOBJ { _modelURL.toLocalFile() }; - - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; - - if (!localOBJ.exists()) { - handleError("Could not find " + _modelURL.toString()); - return; - } - - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localOBJ.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localOBJ.copy(_originalModelFilePath); - - // local OBJ is loaded emit signal to trigger its baking - emit OBJLoaded(); - } else { - // OBJ is remote, start 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(_modelURL); - - qCDebug(model_baking) << "Downloading" << _modelURL; - auto networkReply = networkAccessManager.get(networkRequest); - - connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply); - } -} - -void OBJBaker::handleOBJNetworkReply() { - auto requestReply = qobject_cast(sender()); - - if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded" << _modelURL; - - // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); - - qDebug(model_baking) << "Writing copy of original obj to" << _originalModelFilePath << copyOfOriginal.fileName(); - - if (!copyOfOriginal.open(QIODevice::WriteOnly)) { - // add an error to the error list for this obj stating that a duplicate of the original obj could not be made - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); - return; - } - if (copyOfOriginal.write(requestReply->readAll()) == -1) { - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); - return; - } - - // close that file now that we are done writing to it - copyOfOriginal.close(); - - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - // remote OBJ is loaded emit signal to trigger its baking - emit OBJLoaded(); - } else { - // add an error to our list stating that the OBJ could not be downloaded - handleError("Failed to download " + _modelURL.toString()); - } -} - -void OBJBaker::bakeOBJ() { - // Read the OBJ file - QFile objFile(_originalModelFilePath); - if (!objFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); - return; - } - - QByteArray objData = objFile.readAll(); - - OBJSerializer serializer; - QVariantHash mapping; - mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library - auto geometry = serializer.read(objData, mapping, _modelURL); - +void OBJBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { // Write OBJ Data as FBX tree nodes - createFBXNodeTree(_rootNode, *geometry); - - checkIfTexturesFinished(); + createFBXNodeTree(_rootNode, hfmModel, dracoMeshes[0]); } -void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh) { + // Make all generated nodes children of rootNode + rootNode.children = { FBXNode(), FBXNode(), FBXNode() }; + FBXNode& globalSettingsNode = rootNode.children[0]; + FBXNode& objectNode = rootNode.children[1]; + FBXNode& connectionsNode = rootNode.children[2]; + // Generating FBX Header Node FBXNode headerNode; headerNode.name = FBX_HEADER_EXTENSION; // Generating global settings node // Required for Unit Scale Factor - FBXNode globalSettingsNode; globalSettingsNode.name = GLOBAL_SETTINGS_NODE_NAME; // Setting the tree hierarchy: GlobalSettings -> Properties70 -> P -> Properties - FBXNode properties70Node; - properties70Node.name = PROPERTIES70_NODE_NAME; - - FBXNode pNode; { - pNode.name = P_NODE_NAME; - pNode.properties.append({ - "UnitScaleFactor", "double", "Number", "", - UNIT_SCALE_FACTOR - }); + globalSettingsNode.children.push_back(FBXNode()); + FBXNode& properties70Node = globalSettingsNode.children.back(); + properties70Node.name = PROPERTIES70_NODE_NAME; + + FBXNode pNode; + { + pNode.name = P_NODE_NAME; + pNode.properties.append({ + "UnitScaleFactor", "double", "Number", "", + UNIT_SCALE_FACTOR + }); + } + properties70Node.children = { pNode }; + } - properties70Node.children = { pNode }; - globalSettingsNode.children = { properties70Node }; - // Generating Object node - FBXNode objectNode; objectNode.name = OBJECTS_NODE_NAME; + objectNode.children = { FBXNode(), FBXNode() }; + FBXNode& geometryNode = objectNode.children[0]; + FBXNode& modelNode = objectNode.children[1]; - // Generating Object node's child - Geometry node - FBXNode geometryNode; + // Generating Object node's child - Geometry node geometryNode.name = GEOMETRY_NODE_NAME; NodeID geometryID; { @@ -189,15 +90,8 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { MESH }; } - - // Compress the mesh information and store in dracoNode - bool hasDeformers = false; // No concept of deformers for an OBJ - FBXNode dracoNode; - compressMesh(hfmModel.meshes[0], hasDeformers, dracoNode); - geometryNode.children.append(dracoNode); - + // Generating Object node's child - Model node - FBXNode modelNode; modelNode.name = MODEL_NODE_NAME; NodeID modelID; { @@ -205,16 +99,14 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { modelNode.properties = { modelID, MODEL_NODE_NAME, MESH }; } - objectNode.children = { geometryNode, modelNode }; - // Generating Objects node's child - Material node - auto& meshParts = hfmModel.meshes[0].parts; + auto& meshParts = hfmModel->meshes[0].parts; for (auto& meshPart : meshParts) { FBXNode materialNode; materialNode.name = MATERIAL_NODE_NAME; - if (hfmModel.materials.size() == 1) { + if (hfmModel->materials.size() == 1) { // case when no material information is provided, OBJSerializer considers it as a single default material - for (auto& materialID : hfmModel.materials.keys()) { + for (auto& materialID : hfmModel->materials.keys()) { setMaterialNodeProperties(materialNode, materialID, hfmModel); } } else { @@ -224,12 +116,26 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { objectNode.children.append(materialNode); } + // Store the draco node containing the compressed mesh information, along with the per-meshPart material IDs the draco node references + // Because we redefine the material IDs when initializing the material nodes above, we pass that in for the material list + // The nth mesh part gets the nth material + { + std::vector newMaterialList; + newMaterialList.reserve(_materialIDs.size()); + for (auto materialID : _materialIDs) { + newMaterialList.push_back(hifi::ByteArray(std::to_string((int)materialID).c_str())); + } + FBXNode dracoNode; + buildDracoMeshNode(dracoNode, dracoMesh, newMaterialList); + geometryNode.children.append(dracoNode); + } + // Generating Texture Node // iterate through mesh parts and process the associated textures auto size = meshParts.size(); for (int i = 0; i < size; i++) { QString material = meshParts[i].materialID; - HFMMaterial currentMaterial = hfmModel.materials[material]; + HFMMaterial currentMaterial = hfmModel->materials[material]; if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { auto textureID = nextNodeID(); _mapTextureMaterial.emplace_back(textureID, i); @@ -274,14 +180,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { } // Generating Connections node - FBXNode connectionsNode; connectionsNode.name = CONNECTIONS_NODE_NAME; - // connect Geometry to Model - FBXNode cNode; - cNode.name = C_NODE_NAME; - cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID }; - connectionsNode.children = { cNode }; + // connect Geometry to Model + { + FBXNode cNode; + cNode.name = C_NODE_NAME; + cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID }; + connectionsNode.children.push_back(cNode); + } // connect all materials to model for (auto& materialID : _materialIDs) { @@ -313,18 +220,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) { }; connectionsNode.children.append(cDiffuseNode); } - - // Make all generated nodes children of rootNode - rootNode.children = { globalSettingsNode, objectNode, connectionsNode }; } // Set properties for material nodes -void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel) { +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel) { auto materialID = nextNodeID(); _materialIDs.push_back(materialID); materialNode.properties = { materialID, material, MESH }; - HFMMaterial currentMaterial = hfmModel.materials[material]; + HFMMaterial currentMaterial = hfmModel->materials[material]; // Setting the hierarchy: Material -> Properties70 -> P -> Properties FBXNode properties70Node; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 5aaae49d4a..d1eced5452 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -27,20 +27,12 @@ class OBJBaker : public ModelBaker { public: using ModelBaker::ModelBaker; -public slots: - virtual void bake() override; - -signals: - void OBJLoaded(); - -private slots: - void bakeOBJ(); - void handleOBJNetworkReply(); +protected: + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void loadOBJ(); - void createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel); + void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index af5e59ebbe..202fd4b3d8 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -69,4 +69,4 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB } return baker; -} \ No newline at end of file +} diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 342d605337..362ae93e99 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -30,6 +30,9 @@ static const quint32 FBX_VERSION_2016 = 7500; static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0; static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1; +// The version of the FBX node containing the draco mesh. See also: DRACO_MESH_VERSION in HFM.h +static const int FBX_DRACO_MESH_VERSION = 2; + class FBXNode; using FBXNodeList = QList; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index d5a1f9a562..b4e95a8c2a 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -386,6 +386,8 @@ hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; + bool deduplicateIndices = mapping["deduplicateIndices"].toBool(); + QMap meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; @@ -487,7 +489,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const foreach (const FBXNode& object, child.children) { if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { - meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); + meshes.insert(getID(object.properties), extractMesh(object, meshIndex, deduplicateIndices)); } else { // object.properties.at(2) == "Shape" ExtractedBlendshape extracted = { getID(object.properties), extractBlendshape(object) }; blendshapes.append(extracted); @@ -631,10 +633,10 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const } } } - } else if (subobject.name == "Vertices") { + } else if (subobject.name == "Vertices" || subobject.name == "DracoMesh") { // it's a mesh as well as a model mesh = &meshes[getID(object.properties)]; - *mesh = extractMesh(object, meshIndex); + *mesh = extractMesh(object, meshIndex, deduplicateIndices); } else if (subobject.name == "Shape") { ExtractedBlendshape blendshape = { subobject.properties.at(0).toString(), @@ -1386,9 +1388,9 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const // look for textures, material properties // allocate the Part material library + // NOTE: extracted.partMaterialTextures is empty for FBX_DRACO_MESH_VERSION >= 2. In that case, the mesh part's materialID string is already defined. int materialIndex = 0; int textureIndex = 0; - bool generateTangents = false; QList children = _connectionChildMap.values(modelID); for (int i = children.size() - 1; i >= 0; i--) { @@ -1401,12 +1403,10 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const if (extracted.partMaterialTextures.at(j).first == materialIndex) { HFMMeshPart& part = extracted.mesh.parts[j]; part.materialID = material.materialID; - generateTangents |= material.needTangentSpace(); } } materialIndex++; - } else if (_textureFilenames.contains(childID)) { // NOTE (Sabrina 2019/01/11): getTextures now takes in the materialID as a second parameter, because FBX material nodes can sometimes have uv transform information (ex: "Maya|uv_scale") // I'm leaving the second parameter blank right now as this code may never be used. @@ -1684,5 +1684,7 @@ HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::V _rootNode = parseFBX(&buffer); + // FBXSerializer's mapping parameter supports the bool "deduplicateIndices," which is passed into FBXSerializer::extractMesh as "deduplicate" + return HFMModel::Pointer(extractHFMModel(mapping, url.toString())); } diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index 481f2f4f63..7d41f98444 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -119,7 +119,7 @@ public: HFMModel* extractHFMModel(const hifi::VariantHash& mapping, const QString& url); - static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); + static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate); QHash meshes; HFMTexture getTexture(const QString& textureID, const QString& materialID); diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index f90c4bac6c..2f5286291c 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -345,6 +345,22 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me isDracoMesh = true; data.extracted.mesh.wasCompressed = true; + // Check for additional metadata + unsigned int dracoMeshNodeVersion = 1; + std::vector dracoMaterialList; + for (const auto& dracoChild : child.children) { + if (dracoChild.name == "FBXDracoMeshVersion") { + if (!dracoChild.children.isEmpty()) { + dracoMeshNodeVersion = dracoChild.properties[0].toUInt(); + } + } else if (dracoChild.name == "MaterialList") { + dracoMaterialList.reserve(dracoChild.properties.size()); + for (const auto& materialID : dracoChild.properties) { + dracoMaterialList.push_back(materialID.toString()); + } + } + } + // load the draco mesh from the FBX and create a draco::Mesh draco::Decoder decoder; draco::DecoderBuffer decodedBuffer; @@ -462,8 +478,20 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me // grab or setup the HFMMeshPart for the part this face belongs to int& partIndexPlusOne = materialTextureParts[materialTexture]; if (partIndexPlusOne == 0) { - data.extracted.partMaterialTextures.append(materialTexture); data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1); + HFMMeshPart& part = data.extracted.mesh.parts.back(); + + // Figure out what material this part is + if (dracoMeshNodeVersion >= 2) { + // Define the materialID now + if (dracoMaterialList.size() - 1 <= materialID) { + part.materialID = dracoMaterialList[materialID]; + } + } else { + // Define the materialID later, based on the order of first appearance of the materials in the _connectionChildMap + data.extracted.partMaterialTextures.append(materialTexture); + } + partIndexPlusOne = data.extracted.mesh.parts.size(); } diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 826c79e911..22c9005e98 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -53,6 +53,9 @@ using ColorType = glm::vec3; const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; +// The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h +static const int DRACO_MESH_VERSION = 2; + static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1; diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 4d740f4a94..d7167fa577 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -19,6 +19,7 @@ #include "CalculateBlendshapeNormalsTask.h" #include "CalculateBlendshapeTangentsTask.h" #include "PrepareJointsTask.h" +#include "BuildDracoMeshTask.h" namespace baker { @@ -117,7 +118,7 @@ namespace baker { class BakerEngineBuilder { public: using Input = VaryingSet2; - using Output = VaryingSet2; + using Output = VaryingSet4, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); @@ -156,6 +157,14 @@ namespace baker { // Parse material mapping const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); + // Build Draco meshes + // NOTE: This task is disabled by default and must be enabled through configuration + // TODO: Tangent support (Needs changes to FBXSerializer_Mesh as well) + const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); + const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); + const auto dracoMeshes = buildDracoMeshOutputs.getN(0); + const auto materialList = buildDracoMeshOutputs.getN(1); + // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs); @@ -164,7 +173,7 @@ namespace baker { const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying(); const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); - output = Output(hfmModelOut, materialMapping); + output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList); } }; @@ -174,6 +183,10 @@ namespace baker { _engine->feedInput(1, mapping); } + std::shared_ptr Baker::getConfiguration() { + return _engine->getConfiguration(); + } + void Baker::run() { _engine->run(); } @@ -185,4 +198,12 @@ namespace baker { MaterialMapping Baker::getMaterialMapping() const { return _engine->getOutput().get().get1(); } + + const std::vector& Baker::getDracoMeshes() const { + return _engine->getOutput().get().get2(); + } + + std::vector> Baker::getDracoMaterialLists() const { + return _engine->getOutput().get().get3(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index e8a97b863d..de76c91fc8 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -24,11 +24,16 @@ namespace baker { public: Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping); + std::shared_ptr getConfiguration(); + void run(); // Outputs, available after run() is called hfm::Model::Pointer getHFMModel() const; MaterialMapping getMaterialMapping() const; + const std::vector& getDracoMeshes() const; + // This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials) + std::vector> getDracoMaterialLists() const; protected: EnginePointer _engine; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp new file mode 100644 index 0000000000..9bfd03e218 --- /dev/null +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -0,0 +1,233 @@ +// +// BuildDracoMeshTask.cpp +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/02/20. +// Copyright 2019 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 "BuildDracoMeshTask.h" + +// Fix build warnings due to draco headers not casting size_t +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4267 ) +#endif + +#include +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + +#include "ModelBakerLogging.h" +#include "ModelMath.h" + +std::vector createMaterialList(const hfm::Mesh& mesh) { + std::vector materialList; + for (const auto& meshPart : mesh.parts) { + auto materialID = QVariant(meshPart.materialID).toByteArray(); + const auto materialIt = std::find(materialList.cbegin(), materialList.cend(), materialID); + if (materialIt == materialList.cend()) { + materialList.push_back(materialID); + } + } + return materialList; +} + +std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { + Q_ASSERT(normals.size() == 0 || normals.size() == mesh.vertices.size()); + Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); + Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); + + int64_t numTriangles{ 0 }; + for (auto& part : mesh.parts) { + int extraQuadTriangleIndices = part.quadTrianglesIndices.size() % 3; + int extraTriangleIndices = part.triangleIndices.size() % 3; + if (extraQuadTriangleIndices != 0 || extraTriangleIndices != 0) { + qCWarning(model_baker) << "Found a mesh part with indices not divisible by three. Some indices will be discarded during Draco mesh creation."; + } + numTriangles += (part.quadTrianglesIndices.size() - extraQuadTriangleIndices) / 3; + numTriangles += (part.triangleIndices.size() - extraTriangleIndices) / 3; + } + + if (numTriangles == 0) { + return std::unique_ptr(); + } + + draco::TriangleSoupMeshBuilder meshBuilder; + + meshBuilder.Start(numTriangles); + + bool hasNormals{ normals.size() > 0 }; + bool hasColors{ mesh.colors.size() > 0 }; + bool hasTexCoords{ mesh.texCoords.size() > 0 }; + bool hasTexCoords1{ mesh.texCoords1.size() > 0 }; + bool hasPerFaceMaterials{ mesh.parts.size() > 1 }; + bool needsOriginalIndices{ (!mesh.clusterIndices.empty() || !mesh.blendshapes.empty()) && mesh.originalIndices.size() > 0 }; + + int normalsAttributeID { -1 }; + int colorsAttributeID { -1 }; + int texCoordsAttributeID { -1 }; + int texCoords1AttributeID { -1 }; + int faceMaterialAttributeID { -1 }; + int originalIndexAttributeID { -1 }; + + const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, + 3, draco::DT_FLOAT32); + if (needsOriginalIndices) { + originalIndexAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, + 1, draco::DT_INT32); + } + + if (hasNormals) { + normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, + 3, draco::DT_FLOAT32); + } + if (hasColors) { + colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, + 3, draco::DT_FLOAT32); + } + if (hasTexCoords) { + texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, + 2, draco::DT_FLOAT32); + } + if (hasTexCoords1) { + texCoords1AttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, + 2, draco::DT_FLOAT32); + } + if (hasPerFaceMaterials) { + faceMaterialAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, + 1, draco::DT_UINT16); + } + + auto partIndex = 0; + draco::FaceIndex face; + uint16_t materialID; + + for (auto& part : mesh.parts) { + auto materialIt = std::find(materialList.cbegin(), materialList.cend(), QVariant(part.materialID).toByteArray()); + materialID = (uint16_t)(materialIt - materialList.cbegin()); + + auto addFace = [&](const QVector& indices, int index, draco::FaceIndex face) { + int32_t idx0 = indices[index]; + int32_t idx1 = indices[index + 1]; + int32_t idx2 = indices[index + 2]; + + if (hasPerFaceMaterials) { + meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); + } + + meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, + &mesh.vertices[idx0], &mesh.vertices[idx1], + &mesh.vertices[idx2]); + + if (needsOriginalIndices) { + meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, + &mesh.originalIndices[idx0], + &mesh.originalIndices[idx1], + &mesh.originalIndices[idx2]); + } + if (hasNormals) { + meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, + &normals[idx0], &normals[idx1], + &normals[idx2]); + } + if (hasColors) { + meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, + &mesh.colors[idx0], &mesh.colors[idx1], + &mesh.colors[idx2]); + } + if (hasTexCoords) { + meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, + &mesh.texCoords[idx0], &mesh.texCoords[idx1], + &mesh.texCoords[idx2]); + } + if (hasTexCoords1) { + meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, + &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], + &mesh.texCoords1[idx2]); + } + }; + + for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { + addFace(part.quadTrianglesIndices, i, face++); + } + + for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { + addFace(part.triangleIndices, i, face++); + } + + partIndex++; + } + + auto dracoMesh = meshBuilder.Finalize(); + + if (!dracoMesh) { + qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node"; + return std::unique_ptr(); + } + + // we need to modify unique attribute IDs for custom attributes + // so the attributes are easily retrievable on the other side + if (hasPerFaceMaterials) { + dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); + } + + if (hasTexCoords1) { + dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); + } + + if (needsOriginalIndices) { + dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); + } + + return dracoMesh; +} + +void BuildDracoMeshTask::configure(const Config& config) { + // Nothing to configure yet +} + +void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { + const auto& meshes = input.get0(); + const auto& normalsPerMesh = input.get1(); + const auto& tangentsPerMesh = input.get2(); + auto& dracoBytesPerMesh = output.edit0(); + auto& materialLists = output.edit1(); + + dracoBytesPerMesh.reserve(meshes.size()); + materialLists.reserve(meshes.size()); + for (size_t i = 0; i < meshes.size(); i++) { + const auto& mesh = meshes[i]; + const auto& normals = baker::safeGet(normalsPerMesh, i); + const auto& tangents = baker::safeGet(tangentsPerMesh, i); + dracoBytesPerMesh.emplace_back(); + auto& dracoBytes = dracoBytesPerMesh.back(); + materialLists.push_back(createMaterialList(mesh)); + const auto& materialList = materialLists.back(); + + auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList); + + if (dracoMesh) { + draco::Encoder encoder; + + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); + encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); + encoder.SetSpeedOptions(0, 5); + + draco::EncoderBuffer buffer; + encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); + + dracoBytes = hifi::ByteArray(buffer.data(), (int)buffer.size()); + } + } +} diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h new file mode 100644 index 0000000000..ab1679959a --- /dev/null +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -0,0 +1,39 @@ +// +// BuildDracoMeshTask.h +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/02/20. +// Copyright 2019 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_BuildDracoMeshTask_h +#define hifi_BuildDracoMeshTask_h + +#include +#include + +#include "Engine.h" +#include "BakerTypes.h" + +// BuildDracoMeshTask is disabled by default +class BuildDracoMeshConfig : public baker::JobConfig { + Q_OBJECT +public: + BuildDracoMeshConfig() : baker::JobConfig(false) {} +}; + +class BuildDracoMeshTask { +public: + using Config = BuildDracoMeshConfig; + using Input = baker::VaryingSet3, baker::NormalsPerMesh, baker::TangentsPerMesh>; + using Output = baker::VaryingSet2, std::vector>>; + using JobModel = baker::Job::ModelIO; + + void configure(const Config& config); + void run(const baker::BakeContextPointer& context, const Input& input, Output& output); +}; + +#endif // hifi_BuildDracoMeshTask_h diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index 0dbb9d584d..eecfea5752 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -18,7 +18,7 @@ #include "Engine.h" // The property "passthrough", when enabled, will let the input joints flow to the output unmodified, unlike the disabled property, which discards the data -class PrepareJointsTaskConfig : public baker::JobConfig { +class PrepareJointsConfig : public baker::JobConfig { Q_OBJECT Q_PROPERTY(bool passthrough MEMBER passthrough) public: @@ -27,7 +27,7 @@ public: class PrepareJointsTask { public: - using Config = PrepareJointsTaskConfig; + using Config = PrepareJointsConfig; using Input = baker::VaryingSet2, hifi::VariantHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 1d0032ee4c..ebb53d8ef7 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -246,6 +246,7 @@ void GeometryReader::run() { HFMModel::Pointer hfmModel; QVariantHash serializerMapping = _mapping; serializerMapping["combineParts"] = _combineParts; + serializerMapping["deduplicateIndices"] = true; if (_url.path().toLower().endsWith(".gz")) { QByteArray uncompressedData; diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 18ad37d7b9..c9b1aca1d4 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(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking) +link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking model-baker task) setup_memory_debugger() diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index c70ca27d8b..0a5a989cbf 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -22,6 +22,9 @@ #include #include #include +#include +#include +#include #include "MaterialBaker.h" @@ -43,6 +46,12 @@ Oven::Oven() { MaterialBaker::setNextOvenWorkerThreadOperator([] { return Oven::instance().getNextWorkerThread(); }); + + { + auto modelFormatRegistry = DependencyManager::set(); + modelFormatRegistry->addFormat(FBXSerializer()); + modelFormatRegistry->addFormat(OBJSerializer()); + } } Oven::~Oven() { diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 2b18c07c3a..a5ad5bc891 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -44,10 +44,12 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { try { hifi::ByteArray fbxContents = fbx.readAll(); HFMModel::Pointer hfmModel; + hifi::VariantHash mapping; + mapping["deduplicateIndices"] = true; if (filename.toLower().endsWith(".obj")) { - hfmModel = OBJSerializer().read(fbxContents, QVariantHash(), filename); + hfmModel = OBJSerializer().read(fbxContents, mapping, filename); } else if (filename.toLower().endsWith(".fbx")) { - hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), filename); + hfmModel = FBXSerializer().read(fbxContents, mapping, filename); } else { qWarning() << "file has unknown extension" << filename; return false; From c8648c70161d00e5bb0e2e150bfd467295834046 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 08:53:48 -0800 Subject: [PATCH 019/117] Added worst tile value to test. --- tools/nitpick/src/ImageComparer.cpp | 11 +++++++++++ tools/nitpick/src/ImageComparer.h | 2 ++ tools/nitpick/src/MismatchWindow.cpp | 2 +- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestCreator.cpp | 14 +++++++++----- tools/nitpick/src/TestCreator.h | 3 ++- tools/nitpick/src/common.h | 10 +++++++--- tools/nitpick/ui/MismatchWindow.ui | 6 +++--- 8 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tools/nitpick/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp index 7e3e6eaf63..b35c5d639d 100644 --- a/tools/nitpick/src/ImageComparer.cpp +++ b/tools/nitpick/src/ImageComparer.cpp @@ -43,6 +43,8 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec int windowCounter{ 0 }; double ssim{ 0.0 }; + double worstTileValue{ 1.0 }; + double min { 1.0 }; double max { -1.0 }; @@ -108,6 +110,10 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec if (value < min) min = value; if (value > max) max = value; + if (value < worstTileValue) { + worstTileValue = value; + } + ++windowCounter; y += WIN_SIZE; @@ -122,12 +128,17 @@ void ImageComparer::compareImages(const QImage& resultImage, const QImage& expec _ssimResults.min = min; _ssimResults.max = max; _ssimResults.ssim = ssim / windowCounter; + _ssimResults.worstTileValue = worstTileValue; }; double ImageComparer::getSSIMValue() { return _ssimResults.ssim; } +double ImageComparer::getWorstTileValue() { + return _ssimResults.worstTileValue; +} + SSIMResults ImageComparer::getSSIMResults() { return _ssimResults; } diff --git a/tools/nitpick/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h index fc14dab94d..a18e432a01 100644 --- a/tools/nitpick/src/ImageComparer.h +++ b/tools/nitpick/src/ImageComparer.h @@ -18,7 +18,9 @@ class ImageComparer { public: void compareImages(const QImage& resultImage, const QImage& expectedImage); + double getSSIMValue(); + double getWorstTileValue(); SSIMResults getSSIMResults(); diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp index fd5df0dd4e..2a7aca9f2e 100644 --- a/tools/nitpick/src/MismatchWindow.cpp +++ b/tools/nitpick/src/MismatchWindow.cpp @@ -61,7 +61,7 @@ QPixmap MismatchWindow::computeDiffPixmap(const QImage& expectedImage, const QIm } void MismatchWindow::setTestResult(const TestResult& testResult) { - errorLabel->setText("Similarity: " + QString::number(testResult._error)); + errorLabel->setText("Similarity: " + QString::number(testResult._errorGlobal) + " (worst tile: " + QString::number(testResult._errorLocal) + ")"); imagePath->setText("Path to test: " + testResult._pathname); diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..e72de9d1ad 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - v3.1.3"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..a79a2b3b09 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -83,6 +83,7 @@ int TestCreator::compareImageLists() { QImage expectedImage(_expectedImagesFullFilenames[i]); double similarityIndex; // in [-1.0 .. 1.0], where 1.0 means images are identical + double worstTileValue; // in [-1.0 .. 1.0], where 1.0 means images are identical bool isInteractiveMode = (!_isRunningFromCommandLine && _checkBoxInteractiveMode->isChecked() && !_isRunningInAutomaticTestRun); @@ -93,10 +94,12 @@ int TestCreator::compareImageLists() { } else { _imageComparer.compareImages(resultImage, expectedImage); similarityIndex = _imageComparer.getSSIMValue(); + worstTileValue = _imageComparer.getWorstTileValue(); } TestResult testResult = TestResult{ - (float)similarityIndex, + similarityIndex, + worstTileValue, _expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of result image @@ -105,10 +108,9 @@ int TestCreator::compareImageLists() { _mismatchWindow.setTestResult(testResult); - if (similarityIndex < THRESHOLD) { - ++numberOfFailures; - + if (similarityIndex < THRESHOLD_GLOBAL || worstTileValue < THRESHOLD_LOCAL) { if (!isInteractiveMode) { + ++numberOfFailures; appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); } else { _mismatchWindow.exec(); @@ -117,6 +119,7 @@ int TestCreator::compareImageLists() { case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: + ++numberOfFailures; appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); break; case USER_RESPONSE_ABORT: @@ -198,7 +201,8 @@ void TestCreator::appendTestResultsToFile(const TestResult& testResult, const QP stream << "TestCreator in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' stream << "Expected image was " << testResult._expectedImageFilename << endl; stream << "Actual image was " << testResult._actualImageFilename << endl; - stream << "Similarity index was " << testResult._error << endl; + stream << "Similarity index was " << testResult._errorGlobal << endl; + stream << "Worst tile was " << testResult._errorLocal << endl; descriptionFile.close(); diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index 7cd38b42d4..6491d6fe6c 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -121,7 +121,8 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD{ 0.9999 }; + const double THRESHOLD_GLOBAL{ 0.9999 }; + const double THRESHOLD_LOCAL { 0.7770 }; QDir _imageDirectory; diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index eb228ff2b3..17090c46db 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -18,7 +18,9 @@ public: int width; int height; std::vector results; + double ssim; + double worstTileValue; // Used for scaling double min; @@ -27,15 +29,17 @@ public: class TestResult { public: - TestResult(float error, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : - _error(error), + TestResult(double errorGlobal, double errorLocal, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : + _errorGlobal(errorGlobal), + _errorLocal(errorLocal), _pathname(pathname), _expectedImageFilename(expectedImageFilename), _actualImageFilename(actualImageFilename), _ssimResults(ssimResults) {} - double _error; + double _errorGlobal; + double _errorLocal; QString _pathname; QString _expectedImageFilename; diff --git a/tools/nitpick/ui/MismatchWindow.ui b/tools/nitpick/ui/MismatchWindow.ui index 8a174989d4..fa3e21957f 100644 --- a/tools/nitpick/ui/MismatchWindow.ui +++ b/tools/nitpick/ui/MismatchWindow.ui @@ -45,7 +45,7 @@ - 540 + 900 480 800 450 @@ -78,7 +78,7 @@ 60 630 - 480 + 540 28 @@ -145,7 +145,7 @@ - Abort current test + Abort evaluation From 20e1753605afc1d6dcafd8e1aa306f4bf663c869 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 09:22:52 -0800 Subject: [PATCH 020/117] Reduced threshold a wee bit after testing on laptop. --- tools/nitpick/src/TestCreator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index 6491d6fe6c..c2e7ba14f2 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -121,7 +121,7 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD_GLOBAL{ 0.9999 }; + const double THRESHOLD_GLOBAL{ 0.9998 }; const double THRESHOLD_LOCAL { 0.7770 }; QDir _imageDirectory; From 19c7c26c6369b5df000f68e794c2e5f5834427b6 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 10:04:23 -0800 Subject: [PATCH 021/117] gcc / Mac compilation error. --- tools/nitpick/src/TestCreator.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index a79a2b3b09..f87134ce5b 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -91,6 +91,7 @@ int TestCreator::compareImageLists() { if (isInteractiveMode && (resultImage.width() != expectedImage.width() || resultImage.height() != expectedImage.height())) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); similarityIndex = -100.0; + worstTileValue = 0.0; } else { _imageComparer.compareImages(resultImage, expectedImage); similarityIndex = _imageComparer.getSSIMValue(); From 1e5837f25f5a5a1bffdf36da761040627ce2ceec Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 11:10:54 -0800 Subject: [PATCH 022/117] Enable Android buttons as needed. --- tools/nitpick/src/TestRunnerMobile.cpp | 3 +-- tools/nitpick/ui/Nitpick.ui | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 4d0d18ef3d..d7800f35b4 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -60,6 +60,7 @@ void TestRunnerMobile::setWorkingFolderAndEnableControls() { setWorkingFolder(_workingFolderLabel); _connectDeviceButton->setEnabled(true); + _downloadAPKPushbutton->setEnabled(true); } void TestRunnerMobile::connectDevice() { @@ -154,8 +155,6 @@ void TestRunnerMobile::downloadComplete() { } else { _statusLabel->setText("Installer download complete"); } - - _installAPKPushbutton->setEnabled(true); } void TestRunnerMobile::installAPK() { diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index a0f368863d..c85311d86b 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -46,7 +46,7 @@ - 5 + 0 From dbdf5fdd1f73e1799a9ecd3351ac6dc774ff3ceb Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 13:53:07 -0800 Subject: [PATCH 023/117] Decreased threshold after additional testing. --- tools/nitpick/src/TestCreator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index c2e7ba14f2..f2bd520574 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -122,7 +122,7 @@ private: const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; const double THRESHOLD_GLOBAL{ 0.9998 }; - const double THRESHOLD_LOCAL { 0.7770 }; + const double THRESHOLD_LOCAL { 0.7500 }; QDir _imageDirectory; From 687745dc7e3f3aa44a69db36066840ff0b11ad8f Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 8 Mar 2019 17:17:10 -0800 Subject: [PATCH 024/117] Remove old recursive scripts that are empty. Don't add unneeded delays. --- tools/nitpick/src/TestCreator.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index f87134ce5b..587490bb64 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -824,6 +824,10 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti // If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant if (directories.length() == 0) { + QString testRecursivePathname = directory + "/" + TEST_RECURSIVE_FILENAME; + if (QFile::exists(testRecursivePathname)) { + QFile::remove(testRecursivePathname); + } return; } @@ -856,10 +860,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; From 7419f9899e4af5b753966ea30b259359698d1d0a Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Sat, 9 Mar 2019 11:54:04 -0800 Subject: [PATCH 025/117] Modified thresholds to reduce false positives. --- tools/nitpick/src/TestCreator.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index f2bd520574..b4ce56a7d5 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -121,8 +121,8 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD_GLOBAL{ 0.9998 }; - const double THRESHOLD_LOCAL { 0.7500 }; + const double THRESHOLD_GLOBAL{ 0.9995 }; + const double THRESHOLD_LOCAL { 0.6 }; QDir _imageDirectory; From 97b01bad70d081f3245028618913c4f3e0c5f63a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Mar 2019 12:23:26 -0700 Subject: [PATCH 026/117] tellPhysics to children when animating model --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 03c50008a0..54254ef26c 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1032,7 +1032,7 @@ void RenderableModelEntityItem::copyAnimationJointDataToModel() { }); if (changed) { - locationChanged(false, true); + locationChanged(true, true); } } From c9cb284c19534899224ac9162316b9d811446059 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 11 Mar 2019 14:33:00 -0700 Subject: [PATCH 027/117] Case 21467 - only update search when search field content has changed --- .../qml/hifi/commerce/marketplace/Marketplace.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 07ded49956..fdeca07561 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -359,9 +359,11 @@ Rectangle { } onAccepted: { - root.searchString = searchField.text; - getMarketplaceItems(); - searchField.forceActiveFocus(); + if(root.searchString !== searchField.text) { + root.searchString = searchField.text; + getMarketplaceItems(); + searchField.forceActiveFocus(); + } } onActiveFocusChanged: { From d4b77d15cc4a84ea2eea91cd3eb13ea7da9b5ac8 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 17:11:09 -0700 Subject: [PATCH 028/117] Use correct snapshots folder. Fix bug in APK installer. --- tools/nitpick/src/TestRunnerMobile.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index d7800f35b4..62630cc7b3 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -43,7 +43,7 @@ TestRunnerMobile::TestRunnerMobile( _installAPKPushbutton = installAPKPushbutton; _runInterfacePushbutton = runInterfacePushbutton; - folderLineEdit->setText("/sdcard/DCIM/TEST"); + folderLineEdit->setText("/sdcard/snapshots"); modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; modelNames["SM_N960U1"] = "Samsung Note 9 unlocked"; @@ -163,22 +163,16 @@ void TestRunnerMobile::installAPK() { _adbInterface = new AdbInterface(); } - if (_installerFilename.isNull()) { - QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, - "Available APKs (*.apk)" - ); + QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, + "Available APKs (*.apk)" + ); - if (installerPathname.isNull()) { - return; - } - - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; + if (installerPathname.isNull()) { + return; } _statusLabel->setText("Installing"); - QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); From 41662b183bf82e26fb6b0fdf4fc8ff8015893e07 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 10:45:04 -0700 Subject: [PATCH 029/117] Removed redundant method. --- interface/src/scripting/TestScriptingInterface.cpp | 10 ---------- interface/src/scripting/TestScriptingInterface.h | 7 ------- 2 files changed, 17 deletions(-) diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index a9ba165037..c3aeb2643b 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -199,13 +199,3 @@ void TestScriptingInterface::setOtherAvatarsReplicaCount(int count) { int TestScriptingInterface::getOtherAvatarsReplicaCount() { return qApp->getOtherAvatarsReplicaCount(); } - -QString TestScriptingInterface::getOperatingSystemType() { -#ifdef Q_OS_WIN - return "WINDOWS"; -#elif defined Q_OS_MAC - return "MACOS"; -#else - return "UNKNOWN"; -#endif -} diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 26e967c9b5..4a1d1a3eeb 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -163,13 +163,6 @@ public slots: */ Q_INVOKABLE int getOtherAvatarsReplicaCount(); - /**jsdoc - * Returns the Operating Sytem type - * @function Test.getOperatingSystemType - * @returns {string} "WINDOWS", "MACOS" or "UNKNOWN" - */ - QString getOperatingSystemType(); - private: bool waitForCondition(qint64 maxWaitMs, std::function condition); QString _testResultsLocation; From cb311408c68f2d465a80d76e0fdfe979cde96e13 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 11 Mar 2019 13:15:47 -0700 Subject: [PATCH 030/117] Remove _compositeFramebuffer from display plugins --- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 6 +- .../Basic2DWindowOpenGLDisplayPlugin.h | 2 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 175 ++++++++---------- .../src/display-plugins/OpenGLDisplayPlugin.h | 28 ++- .../hmd/DebugHmdDisplayPlugin.h | 2 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 64 +++---- .../display-plugins/hmd/HmdDisplayPlugin.h | 11 +- .../stereo/InterleavedStereoDisplayPlugin.cpp | 4 +- .../stereo/InterleavedStereoDisplayPlugin.h | 2 +- .../src/OculusMobileDisplayPlugin.cpp | 10 +- .../src/OculusMobileDisplayPlugin.h | 4 +- .../plugins/src/plugins/DisplayPlugin.cpp | 12 +- libraries/plugins/src/plugins/DisplayPlugin.h | 8 +- .../render-utils/src/RenderCommonTask.cpp | 4 +- libraries/render/src/render/Args.h | 2 +- plugins/oculus/src/OculusDebugDisplayPlugin.h | 2 +- plugins/oculus/src/OculusDisplayPlugin.cpp | 28 ++- plugins/oculus/src/OculusDisplayPlugin.h | 2 +- .../src/OculusLegacyDisplayPlugin.cpp | 4 +- .../src/OculusLegacyDisplayPlugin.h | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 16 +- plugins/openvr/src/OpenVrDisplayPlugin.h | 4 +- 22 files changed, 181 insertions(+), 211 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index 9828a8beda..f55f5919f5 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -109,7 +109,7 @@ bool Basic2DWindowOpenGLDisplayPlugin::internalActivate() { return Parent::internalActivate(); } -void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { +void Basic2DWindowOpenGLDisplayPlugin::compositeExtra(const gpu::FramebufferPointer& compositeFramebuffer) { #if defined(Q_OS_ANDROID) auto& virtualPadManager = VirtualPad::Manager::instance(); if(virtualPadManager.getLeftVirtualPad()->isShown()) { @@ -121,7 +121,7 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); + batch.setFramebuffer(compositeFramebuffer); batch.resetViewTransform(); batch.setProjectionTransform(mat4()); batch.setPipeline(_cursorPipeline); @@ -140,7 +140,7 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { }); } #endif - Parent::compositeExtra(); + Parent::compositeExtra(compositeFramebuffer); } static const uint32_t MIN_THROTTLE_CHECK_FRAMES = 60; diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index cc304c19c2..d4c321a571 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -33,7 +33,7 @@ public: virtual bool isThrottled() const override; - virtual void compositeExtra() override; + virtual void compositeExtra(const gpu::FramebufferPointer&) override; virtual void pluginUpdate() override {}; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index c536e6b6e2..5bc84acc6a 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -379,14 +379,6 @@ void OpenGLDisplayPlugin::customizeContext() { scissorState->setDepthTest(gpu::State::DepthTest(false)); scissorState->setScissorEnable(true); - { -#ifdef Q_OS_ANDROID - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); -#else - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); -#endif - _simplePipeline = gpu::Pipeline::create(program, scissorState); - } { #ifdef Q_OS_ANDROID gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); @@ -396,29 +388,59 @@ void OpenGLDisplayPlugin::customizeContext() { _presentPipeline = gpu::Pipeline::create(program, scissorState); } + + // HUD operator { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); - _hudPipeline = gpu::Pipeline::create(program, blendState); - } - { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureMirroredX); - _mirrorHUDPipeline = gpu::Pipeline::create(program, blendState); + gpu::PipelinePointer hudPipeline; + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); + hudPipeline = gpu::Pipeline::create(program, blendState); + } + + gpu::PipelinePointer hudMirrorPipeline; + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureMirroredX); + hudMirrorPipeline = gpu::Pipeline::create(program, blendState); + } + + + _hudOperator = [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, const gpu::FramebufferPointer& compositeFramebuffer, bool mirror) { + auto hudStereo = isStereo(); + auto hudCompositeFramebufferSize = compositeFramebuffer->getSize(); + std::array hudEyeViewports; + for_each_eye([&](Eye eye) { + hudEyeViewports[eye] = eyeViewport(eye); + }); + if (hudPipeline && hudTexture) { + batch.enableStereo(false); + batch.setPipeline(mirror ? hudMirrorPipeline : hudPipeline); + batch.setResourceTexture(0, hudTexture); + if (hudStereo) { + for_each_eye([&](Eye eye) { + batch.setViewportTransform(hudEyeViewports[eye]); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + } else { + batch.setViewportTransform(ivec4(uvec2(0), hudCompositeFramebufferSize)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + } + } + }; + } + { gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTransformedTexture); _cursorPipeline = gpu::Pipeline::create(program, blendState); } } - updateCompositeFramebuffer(); } void OpenGLDisplayPlugin::uncustomizeContext() { _presentPipeline.reset(); _cursorPipeline.reset(); - _hudPipeline.reset(); - _mirrorHUDPipeline.reset(); - _compositeFramebuffer.reset(); + _hudOperator = DEFAULT_HUD_OPERATOR; withPresentThreadLock([&] { _currentFrame.reset(); _lastFrame = nullptr; @@ -510,24 +532,16 @@ void OpenGLDisplayPlugin::captureFrame(const std::string& filename) const { }); } -void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor) { - renderFromTexture(batch, texture, viewport, scissor, nullptr); -} -void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& copyFbo /*=gpu::FramebufferPointer()*/) { - auto fbo = gpu::FramebufferPointer(); +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& destFbo, const gpu::FramebufferPointer& copyFbo /*=gpu::FramebufferPointer()*/) { batch.enableStereo(false); batch.resetViewTransform(); - batch.setFramebuffer(fbo); + batch.setFramebuffer(destFbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(scissor); batch.setViewportTransform(viewport); batch.setResourceTexture(0, texture); -#ifndef USE_GLES batch.setPipeline(_presentPipeline); -#else - batch.setPipeline(_simplePipeline); -#endif batch.draw(gpu::TRIANGLE_STRIP, 4); if (copyFbo) { gpu::Vec4i copyFboRect(0, 0, copyFbo->getWidth(), copyFbo->getHeight()); @@ -553,7 +567,7 @@ void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::Textur batch.setViewportTransform(copyFboRect); batch.setStateScissorRect(copyFboRect); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, {0.0f, 0.0f, 0.0f, 1.0f}); - batch.blit(fbo, sourceRect, copyFbo, copyRect); + batch.blit(destFbo, sourceRect, copyFbo, copyRect); } } @@ -581,41 +595,14 @@ void OpenGLDisplayPlugin::updateFrameData() { }); } -std::function OpenGLDisplayPlugin::getHUDOperator() { - auto hudPipeline = _hudPipeline; - auto hudMirrorPipeline = _mirrorHUDPipeline; - auto hudStereo = isStereo(); - auto hudCompositeFramebufferSize = _compositeFramebuffer->getSize(); - std::array hudEyeViewports; - for_each_eye([&](Eye eye) { - hudEyeViewports[eye] = eyeViewport(eye); - }); - return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, bool mirror) { - if (hudPipeline && hudTexture) { - batch.enableStereo(false); - batch.setPipeline(mirror ? hudMirrorPipeline : hudPipeline); - batch.setResourceTexture(0, hudTexture); - if (hudStereo) { - for_each_eye([&](Eye eye) { - batch.setViewportTransform(hudEyeViewports[eye]); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - } else { - batch.setViewportTransform(ivec4(uvec2(0), hudCompositeFramebufferSize)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - } - }; -} - -void OpenGLDisplayPlugin::compositePointer() { +void OpenGLDisplayPlugin::compositePointer(const gpu::FramebufferPointer& compositeFramebuffer) { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto cursorTransform = DependencyManager::get()->getReticleTransform(glm::mat4()); render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.setProjectionTransform(mat4()); - batch.setFramebuffer(_compositeFramebuffer); + batch.setFramebuffer(compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.resetViewTransform(); @@ -626,34 +613,13 @@ void OpenGLDisplayPlugin::compositePointer() { batch.draw(gpu::TRIANGLE_STRIP, 4); }); } else { - batch.setViewportTransform(ivec4(uvec2(0), _compositeFramebuffer->getSize())); + batch.setViewportTransform(ivec4(uvec2(0), compositeFramebuffer->getSize())); batch.draw(gpu::TRIANGLE_STRIP, 4); } }); } -void OpenGLDisplayPlugin::compositeScene() { - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); - batch.setViewportTransform(ivec4(uvec2(), _compositeFramebuffer->getSize())); - batch.setStateScissorRect(ivec4(uvec2(), _compositeFramebuffer->getSize())); - batch.resetViewTransform(); - batch.setProjectionTransform(mat4()); - batch.setPipeline(_simplePipeline); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); -} - -void OpenGLDisplayPlugin::compositeLayers() { - updateCompositeFramebuffer(); - - { - PROFILE_RANGE_EX(render_detail, "compositeScene", 0xff0077ff, (uint64_t)presentCount()) - compositeScene(); - } - +void OpenGLDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& compositeFramebuffer) { #ifdef HIFI_ENABLE_NSIGHT_DEBUG if (false) // do not draw the HUD if running nsight debug #endif @@ -667,23 +633,35 @@ void OpenGLDisplayPlugin::compositeLayers() { { PROFILE_RANGE_EX(render_detail, "compositeExtra", 0xff0077ff, (uint64_t)presentCount()) - compositeExtra(); + compositeExtra(compositeFramebuffer); } // Draw the pointer last so it's on top of everything auto compositorHelper = DependencyManager::get(); if (compositorHelper->getReticleVisible()) { PROFILE_RANGE_EX(render_detail, "compositePointer", 0xff0077ff, (uint64_t)presentCount()) - compositePointer(); + compositePointer(compositeFramebuffer); } } -void OpenGLDisplayPlugin::internalPresent() { +void OpenGLDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { render([&](gpu::Batch& batch) { // Note: _displayTexture must currently be the same size as the display. uvec2 dims = _displayTexture ? uvec2(_displayTexture->getDimensions()) : getSurfacePixels(); auto viewport = ivec4(uvec2(0), dims); - renderFromTexture(batch, _displayTexture ? _displayTexture : _compositeFramebuffer->getRenderBuffer(0), viewport, viewport); + + gpu::TexturePointer finalTexture; + if (_displayTexture) { + finalTexture = _displayTexture; + } else if (compositeFramebuffer) { + finalTexture = compositeFramebuffer->getRenderBuffer(0); + } else { + qCWarning(displayPlugins) << "No valid texture for output"; + } + + if (finalTexture) { + renderFromTexture(batch, finalTexture, viewport, viewport); + } }); swapBuffers(); _presentRate.increment(); @@ -700,7 +678,7 @@ void OpenGLDisplayPlugin::present() { } incrementPresentCount(); - if (_currentFrame) { + if (_currentFrame && _currentFrame->framebuffer) { auto correction = getViewCorrection(); getGLBackend()->setCameraCorrection(correction, _prevRenderView); _prevRenderView = correction * _currentFrame->view; @@ -720,18 +698,18 @@ void OpenGLDisplayPlugin::present() { // Write all layers to a local framebuffer { PROFILE_RANGE_EX(render, "composite", 0xff00ffff, frameId) - compositeLayers(); + compositeLayers(_currentFrame->framebuffer); } // Take the composite framebuffer and send it to the output device { PROFILE_RANGE_EX(render, "internalPresent", 0xff00ffff, frameId) - internalPresent(); + internalPresent(_currentFrame->framebuffer); } gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); } else if (alwaysPresent()) { - internalPresent(); + internalPresent(nullptr); } _movingAveragePresent.addSample((float)(usecTimestampNow() - startPresent)); } @@ -788,7 +766,12 @@ bool OpenGLDisplayPlugin::setDisplayTexture(const QString& name) { } QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { - auto size = _compositeFramebuffer->getSize(); + if (!_currentFrame || !_currentFrame->framebuffer) { + return QImage(); + } + + auto compositeFramebuffer = _currentFrame->framebuffer; + auto size = compositeFramebuffer->getSize(); if (isHmd()) { size.x /= 2; } @@ -806,7 +789,7 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { auto glBackend = const_cast(*this).getGLBackend(); QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32); withOtherThreadContext([&] { - glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); + glBackend->downloadFramebuffer(compositeFramebuffer, ivec4(corner, bestSize), screenshot); }); return screenshot.mirrored(false, true); } @@ -858,7 +841,7 @@ bool OpenGLDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } ivec4 OpenGLDisplayPlugin::eyeViewport(Eye eye) const { - uvec2 vpSize = _compositeFramebuffer->getSize(); + auto vpSize = glm::uvec2(getRecommendedRenderSize()); vpSize.x /= 2; uvec2 vpPos; if (eye == Eye::Right) { @@ -891,14 +874,6 @@ void OpenGLDisplayPlugin::render(std::function f) { OpenGLDisplayPlugin::~OpenGLDisplayPlugin() { } -void OpenGLDisplayPlugin::updateCompositeFramebuffer() { - auto renderSize = glm::uvec2(getRecommendedRenderSize()); - if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { - _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); - // _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_SRGBA_32, renderSize.x, renderSize.y)); - } -} - void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) { #if !defined(USE_GLES) auto glBackend = const_cast(*this).getGLBackend(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 49a38ecb4c..3c48e8fc48 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -94,14 +94,10 @@ protected: // is not populated virtual bool alwaysPresent() const { return false; } - void updateCompositeFramebuffer(); - virtual QThread::Priority getPresentPriority() { return QThread::HighPriority; } - virtual void compositeLayers(); - virtual void compositeScene(); - virtual std::function getHUDOperator(); - virtual void compositePointer(); - virtual void compositeExtra() {}; + virtual void compositeLayers(const gpu::FramebufferPointer&); + virtual void compositePointer(const gpu::FramebufferPointer&); + virtual void compositeExtra(const gpu::FramebufferPointer&) {}; // These functions must only be called on the presentation thread virtual void customizeContext(); @@ -116,10 +112,10 @@ protected: virtual void deactivateSession() {} // Plugin specific functionality to send the composed scene to the output window or device - virtual void internalPresent(); + virtual void internalPresent(const gpu::FramebufferPointer&); - void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& fbo); - void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor); + + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer& texture, const glm::ivec4& viewport, const glm::ivec4& scissor, const gpu::FramebufferPointer& destFbo = nullptr, const gpu::FramebufferPointer& copyFbo = nullptr); virtual void updateFrameData(); virtual glm::mat4 getViewCorrection() { return glm::mat4(); } @@ -142,14 +138,8 @@ protected: gpu::FramePointer _currentFrame; gpu::Frame* _lastFrame { nullptr }; mat4 _prevRenderView; - gpu::FramebufferPointer _compositeFramebuffer; - gpu::PipelinePointer _hudPipeline; - gpu::PipelinePointer _mirrorHUDPipeline; - gpu::ShaderPointer _mirrorHUDPS; - gpu::PipelinePointer _simplePipeline; - gpu::PipelinePointer _presentPipeline; gpu::PipelinePointer _cursorPipeline; - gpu::TexturePointer _displayTexture{}; + gpu::TexturePointer _displayTexture; float _compositeHUDAlpha { 1.0f }; struct CursorData { @@ -185,5 +175,9 @@ protected: // be serialized through this mutex mutable Mutex _presentMutex; float _hudAlpha{ 1.0f }; + +private: + gpu::PipelinePointer _presentPipeline; + }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h index f2b1f36419..95592cc490 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h @@ -24,7 +24,7 @@ public: protected: void updatePresentPose() override; - void hmdPresent() override {} + void hmdPresent(const gpu::FramebufferPointer&) override {} bool isHmdMounted() const override { return true; } bool internalActivate() override; private: diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 321bcc3fd2..a515232b3f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -114,20 +114,23 @@ void HmdDisplayPlugin::internalDeactivate() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); - _hudRenderer.build(); + _hudOperator = _hudRenderer.build(); } void HmdDisplayPlugin::uncustomizeContext() { // This stops the weirdness where if the preview was disabled, on switching back to 2D, // the vsync was stuck in the disabled state. No idea why that happens though. _disablePreview = false; - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(_compositeFramebuffer); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - }); - _hudRenderer = HUDRenderer(); + if (_currentFrame && _currentFrame->framebuffer) { + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + }); + + } + _hudRenderer = {}; _previewTexture.reset(); Parent::uncustomizeContext(); } @@ -174,11 +177,11 @@ float HmdDisplayPlugin::getLeftCenterPixel() const { return leftCenterPixel; } -void HmdDisplayPlugin::internalPresent() { +void HmdDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { PROFILE_RANGE_EX(render, __FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) // Composite together the scene, hud and mouse cursor - hmdPresent(); + hmdPresent(compositeFramebuffer); if (_displayTexture) { // Note: _displayTexture must currently be the same size as the display. @@ -260,7 +263,7 @@ void HmdDisplayPlugin::internalPresent() { viewport.z *= 2; } - renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor, fbo); + renderFromTexture(batch, compositeFramebuffer->getRenderBuffer(0), viewport, scissor, nullptr, fbo); }); swapBuffers(); @@ -345,7 +348,7 @@ glm::mat4 HmdDisplayPlugin::getViewCorrection() { } } -void HmdDisplayPlugin::HUDRenderer::build() { +DisplayPlugin::HUDOperator HmdDisplayPlugin::HUDRenderer::build() { vertices = std::make_shared(); indices = std::make_shared(); @@ -380,7 +383,7 @@ void HmdDisplayPlugin::HUDRenderer::build() { indexCount = numberOfRectangles * TRIANGLE_PER_RECTANGLE * VERTEX_PER_TRANGLE; // Compute indices order - std::vector indices; + std::vector indexData; for (int i = 0; i < stacks - 1; i++) { for (int j = 0; j < slices - 1; j++) { GLushort bottomLeftIndex = i * slices + j; @@ -388,24 +391,21 @@ void HmdDisplayPlugin::HUDRenderer::build() { GLushort topLeftIndex = bottomLeftIndex + slices; GLushort topRightIndex = topLeftIndex + 1; // FIXME make a z-order curve for better vertex cache locality - indices.push_back(topLeftIndex); - indices.push_back(bottomLeftIndex); - indices.push_back(topRightIndex); + indexData.push_back(topLeftIndex); + indexData.push_back(bottomLeftIndex); + indexData.push_back(topRightIndex); - indices.push_back(topRightIndex); - indices.push_back(bottomLeftIndex); - indices.push_back(bottomRightIndex); + indexData.push_back(topRightIndex); + indexData.push_back(bottomLeftIndex); + indexData.push_back(bottomRightIndex); } } - this->indices->append(indices); + indices->append(indexData); format = std::make_shared(); // 1 for everyone format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); format->setAttribute(gpu::Stream::TEXCOORD, gpu::Stream::TEXCOORD, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); uniformsBuffer = std::make_shared(sizeof(Uniforms), nullptr); - updatePipeline(); -} -void HmdDisplayPlugin::HUDRenderer::updatePipeline() { if (!pipeline) { auto program = gpu::Shader::createProgram(shader::render_utils::program::hmd_ui); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -416,10 +416,6 @@ void HmdDisplayPlugin::HUDRenderer::updatePipeline() { pipeline = gpu::Pipeline::create(program, state); } -} - -std::function HmdDisplayPlugin::HUDRenderer::render(HmdDisplayPlugin& plugin) { - updatePipeline(); auto hudPipeline = pipeline; auto hudFormat = format; @@ -428,9 +424,9 @@ std::function HmdDis auto hudUniformBuffer = uniformsBuffer; auto hudUniforms = uniforms; auto hudIndexCount = indexCount; - return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, bool mirror) { - if (hudPipeline && hudTexture) { - batch.setPipeline(hudPipeline); + return [=](gpu::Batch& batch, const gpu::TexturePointer& hudTexture, const gpu::FramebufferPointer&, const bool mirror) { + if (pipeline && hudTexture) { + batch.setPipeline(pipeline); batch.setInputFormat(hudFormat); gpu::BufferView posView(hudVertices, VERTEX_OFFSET, hudVertices->getSize(), VERTEX_STRIDE, hudFormat->getAttributes().at(gpu::Stream::POSITION)._element); @@ -454,7 +450,7 @@ std::function HmdDis }; } -void HmdDisplayPlugin::compositePointer() { +void HmdDisplayPlugin::compositePointer(const gpu::FramebufferPointer& compositeFramebuffer) { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto compositorHelper = DependencyManager::get(); @@ -463,7 +459,7 @@ void HmdDisplayPlugin::compositePointer() { render([&](gpu::Batch& batch) { // FIXME use standard gpu stereo rendering for this. batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); + batch.setFramebuffer(compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.resetViewTransform(); @@ -478,10 +474,6 @@ void HmdDisplayPlugin::compositePointer() { }); } -std::function HmdDisplayPlugin::getHUDOperator() { - return _hudRenderer.render(*this); -} - HmdDisplayPlugin::~HmdDisplayPlugin() { } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index d8c0ce8e1d..6755c5b7e0 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -53,16 +53,15 @@ signals: void hmdVisibleChanged(bool visible); protected: - virtual void hmdPresent() = 0; + virtual void hmdPresent(const gpu::FramebufferPointer&) = 0; virtual bool isHmdMounted() const = 0; virtual void postPreview() {}; virtual void updatePresentPose(); bool internalActivate() override; void internalDeactivate() override; - std::function getHUDOperator() override; - void compositePointer() override; - void internalPresent() override; + void compositePointer(const gpu::FramebufferPointer&) override; + void internalPresent(const gpu::FramebufferPointer&) override; void customizeContext() override; void uncustomizeContext() override; void updateFrameData() override; @@ -120,8 +119,6 @@ private: static const size_t TEXTURE_OFFSET { offsetof(Vertex, uv) }; static const int VERTEX_STRIDE { sizeof(Vertex) }; - void build(); - void updatePipeline(); - std::function render(HmdDisplayPlugin& plugin); + HUDOperator build(); } _hudRenderer; }; diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp index 0ae0f9b1b6..69aa7fc344 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp @@ -37,13 +37,13 @@ glm::uvec2 InterleavedStereoDisplayPlugin::getRecommendedRenderSize() const { return result; } -void InterleavedStereoDisplayPlugin::internalPresent() { +void InterleavedStereoDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compositeFramebuffer) { render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.resetViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.setResourceTexture(0, compositeFramebuffer->getRenderBuffer(0)); batch.setPipeline(_interleavedPresentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); }); diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h index debd340f24..52dfa8f402 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h @@ -21,7 +21,7 @@ protected: // initialize OpenGL context settings needed by the plugin void customizeContext() override; void uncustomizeContext() override; - void internalPresent() override; + void internalPresent(const gpu::FramebufferPointer&) override; private: static const QString NAME; diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index 9809d02866..12a9b12adc 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -245,7 +245,7 @@ void OculusMobileDisplayPlugin::updatePresentPose() { }); } -void OculusMobileDisplayPlugin::internalPresent() { +void OculusMobileDisplayPlugin::internalPresent(const gpu::FramebufferPointer& compsiteFramebuffer) { VrHandler::pollTask(); if (!vrActive()) { @@ -253,8 +253,12 @@ void OculusMobileDisplayPlugin::internalPresent() { return; } - auto sourceTexture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); - glm::uvec2 sourceSize{ _compositeFramebuffer->getWidth(), _compositeFramebuffer->getHeight() }; + GLuint sourceTexture = 0; + glm::uvec2 sourceSize; + if (compsiteFramebuffer) { + sourceTexture = getGLBackend()->getTextureID(compsiteFramebuffer->getRenderBuffer(0)); + sourceSize = { compsiteFramebuffer->getWidth(), compsiteFramebuffer->getHeight() }; + } VrHandler::presentFrame(sourceTexture, sourceSize, presentTracking); _presentRate.increment(); } diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h index a98989655e..b5f7aa57b0 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h @@ -54,8 +54,8 @@ protected: void uncustomizeContext() override; void updatePresentPose() override; - void internalPresent() override; - void hmdPresent() override { throw std::runtime_error("Unused"); } + void internalPresent(const gpu::FramebufferPointer&) override; + void hmdPresent(const gpu::FramebufferPointer&) override { throw std::runtime_error("Unused"); } bool isHmdMounted() const override; bool alwaysPresent() const override { return true; } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index 47503e8f85..71db87557c 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -2,6 +2,12 @@ #include + +const DisplayPlugin::HUDOperator DisplayPlugin::DEFAULT_HUD_OPERATOR{ std::function() }; + +DisplayPlugin::DisplayPlugin() : _hudOperator{ DEFAULT_HUD_OPERATOR } { +} + int64_t DisplayPlugin::getPaintDelayUsecs() const { std::lock_guard lock(_paintDelayMutex); return _paintDelayTimer.isValid() ? _paintDelayTimer.nsecsElapsed() / NSECS_PER_USEC : 0; @@ -35,8 +41,8 @@ void DisplayPlugin::waitForPresent() { } } -std::function DisplayPlugin::getHUDOperator() { - std::function hudOperator; +std::function DisplayPlugin::getHUDOperator() { + HUDOperator hudOperator; { QMutexLocker locker(&_presentMutex); hudOperator = _hudOperator; @@ -48,3 +54,5 @@ glm::mat4 HmdDisplay::getEyeToHeadTransform(Eye eye) const { static const glm::mat4 xform; return xform; } + + diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index aa52e57c3f..9194fde3ac 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -121,6 +121,8 @@ class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: + DisplayPlugin(); + virtual int getRequiredThreadCount() const { return 0; } virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } @@ -214,7 +216,8 @@ public: void waitForPresent(); float getAveragePresentTime() { return _movingAveragePresent.average / (float)USECS_PER_MSEC; } // in msec - std::function getHUDOperator(); + using HUDOperator = std::function; + virtual HUDOperator getHUDOperator() final; static const QString& MENU_PATH(); @@ -231,7 +234,8 @@ protected: gpu::ContextPointer _gpuContext; - std::function _hudOperator { std::function() }; + static const HUDOperator DEFAULT_HUD_OPERATOR; + HUDOperator _hudOperator; MovingAverage _movingAveragePresent; diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 385e384efe..e77ccb74a5 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -122,8 +122,8 @@ void CompositeHUD::run(const RenderContextPointer& renderContext, const gpu::Fra if (inputs) { batch.setFramebuffer(inputs); } - if (renderContext->args->_hudOperator) { - renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); + if (renderContext->args->_hudOperator && renderContext->args->_blitFramebuffer) { + renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_blitFramebuffer, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); } }); #endif diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h index b5c98e3428..8b2fff68c6 100644 --- a/libraries/render/src/render/Args.h +++ b/libraries/render/src/render/Args.h @@ -131,7 +131,7 @@ namespace render { render::ScenePointer _scene; int8_t _cameraMode { -1 }; - std::function _hudOperator; + std::function _hudOperator; gpu::TexturePointer _hudTexture; }; diff --git a/plugins/oculus/src/OculusDebugDisplayPlugin.h b/plugins/oculus/src/OculusDebugDisplayPlugin.h index ec05cd92e2..690a488b34 100644 --- a/plugins/oculus/src/OculusDebugDisplayPlugin.h +++ b/plugins/oculus/src/OculusDebugDisplayPlugin.h @@ -16,7 +16,7 @@ public: bool isSupported() const override; protected: - void hmdPresent() override {} + void hmdPresent(const gpu::FramebufferPointer&) override {} bool isHmdMounted() const override { return true; } private: diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index df01591639..48440ac80f 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -108,13 +108,16 @@ void OculusDisplayPlugin::customizeContext() { } void OculusDisplayPlugin::uncustomizeContext() { + #if 0 - // Present a final black frame to the HMD - _compositeFramebuffer->Bound(FramebufferTarget::Draw, [] { - Context::ClearColor(0, 0, 0, 1); - Context::Clear().ColorBuffer(); - }); - hmdPresent(); + if (_currentFrame && _currentFrame->framebuffer) { + // Present a final black frame to the HMD + _currentFrame->framebuffer->Bound(FramebufferTarget::Draw, [] { + Context::ClearColor(0, 0, 0, 1); + Context::Clear().ColorBuffer(); + }); + hmdPresent(); + } #endif ovr_DestroyTextureSwapChain(_session, _textureSwapChain); @@ -127,7 +130,7 @@ void OculusDisplayPlugin::uncustomizeContext() { static const uint64_t FRAME_BUDGET = (11 * USECS_PER_MSEC); static const uint64_t FRAME_OVER_BUDGET = (15 * USECS_PER_MSEC); -void OculusDisplayPlugin::hmdPresent() { +void OculusDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { static uint64_t lastSubmitEnd = 0; if (!_customized) { @@ -157,15 +160,8 @@ void OculusDisplayPlugin::hmdPresent() { auto fbo = getGLBackend()->getFramebufferID(_outputFramebuffer); glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, curTexId, 0); render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.setFramebuffer(_outputFramebuffer); - batch.setViewportTransform(ivec4(uvec2(), _outputFramebuffer->getSize())); - batch.setStateScissorRect(ivec4(uvec2(), _outputFramebuffer->getSize())); - batch.resetViewTransform(); - batch.setProjectionTransform(mat4()); - batch.setPipeline(_presentPipeline); - batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); + auto viewport = ivec4(uvec2(), _outputFramebuffer->getSize()); + renderFromTexture(batch, compositeFramebuffer->getRenderBuffer(0), viewport, viewport, _outputFramebuffer); }); glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, 0, 0); } diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index 9209fd373e..a0126d2e58 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -28,7 +28,7 @@ protected: QThread::Priority getPresentPriority() override { return QThread::TimeCriticalPriority; } bool internalActivate() override; - void hmdPresent() override; + void hmdPresent(const gpu::FramebufferPointer&) override; bool isHmdMounted() const override; void customizeContext() override; void uncustomizeContext() override; diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index e6b555443f..a928887866 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -237,7 +237,7 @@ void OculusLegacyDisplayPlugin::uncustomizeContext() { Parent::uncustomizeContext(); } -void OculusLegacyDisplayPlugin::hmdPresent() { +void OculusLegacyDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { if (!_hswDismissed) { ovrHSWDisplayState hswState; ovrHmd_GetHSWDisplayState(_hmd, &hswState); @@ -252,7 +252,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLint texture = getGLBackend()->getTextureID(compositeFramebuffer->getRenderBuffer(0)); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 36bdd1c792..241d626f0c 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -39,7 +39,7 @@ protected: void customizeContext() override; void uncustomizeContext() override; - void hmdPresent() override; + void hmdPresent(const gpu::FramebufferPointer&) override; bool isHmdMounted() const override { return true; } private: diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 11d941dcd0..3d22268472 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -511,13 +511,13 @@ void OpenVrDisplayPlugin::customizeContext() { Parent::customizeContext(); if (_threadedSubmit) { - _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); +// _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { - if (0 != i) { +// if (0 != i) { _compositeInfos[i].texture = gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT)); - } +// } _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } _submitThread->_canvas = _submitCanvas; @@ -613,17 +613,17 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { return Parent::beginFrameRender(frameIndex); } -void OpenVrDisplayPlugin::compositeLayers() { +void OpenVrDisplayPlugin::compositeLayers(const gpu::FramebufferPointer& compositeFramebuffer) { if (_threadedSubmit) { ++_renderingIndex; _renderingIndex %= COMPOSITING_BUFFER_SIZE; auto& newComposite = _compositeInfos[_renderingIndex]; newComposite.pose = _currentPresentFrameInfo.presentPose; - _compositeFramebuffer->setRenderBuffer(0, newComposite.texture); + compositeFramebuffer->setRenderBuffer(0, newComposite.texture); } - Parent::compositeLayers(); + Parent::compositeLayers(compositeFramebuffer); if (_threadedSubmit) { auto& newComposite = _compositeInfos[_renderingIndex]; @@ -645,13 +645,13 @@ void OpenVrDisplayPlugin::compositeLayers() { } } -void OpenVrDisplayPlugin::hmdPresent() { +void OpenVrDisplayPlugin::hmdPresent(const gpu::FramebufferPointer& compositeFramebuffer) { PROFILE_RANGE_EX(render, __FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + GLuint glTexId = getGLBackend()->getTextureID(compositeFramebuffer->getRenderBuffer(0)); vr::Texture_t vrTexture{ (void*)(uintptr_t)glTexId, vr::TextureType_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 265f328920..923a0f7a8f 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -72,8 +72,8 @@ protected: void internalDeactivate() override; void updatePresentPose() override; - void compositeLayers() override; - void hmdPresent() override; + void compositeLayers(const gpu::FramebufferPointer&) override; + void hmdPresent(const gpu::FramebufferPointer&) override; bool isHmdMounted() const override; void postPreview() override; From 6303f61cc32b010e3a73278e73ccd37cfcb39b64 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Tue, 12 Mar 2019 14:26:59 -0700 Subject: [PATCH 031/117] fix lasers scale issue --- interface/src/avatar/MyAvatar.h | 1 + libraries/shared/src/NestableTransformNode.h | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e516364f61..917da1a852 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1122,6 +1122,7 @@ public: float getUserEyeHeight() const; virtual SpatialParentTree* getParentTree() const override; + virtual glm::vec3 scaleForChildren() const override { return glm::vec3(getSensorToWorldScale()); } const QUuid& getSelfID() const { return AVATAR_SELF_ID; } diff --git a/libraries/shared/src/NestableTransformNode.h b/libraries/shared/src/NestableTransformNode.h index a584bcd308..f70d158c91 100644 --- a/libraries/shared/src/NestableTransformNode.h +++ b/libraries/shared/src/NestableTransformNode.h @@ -20,8 +20,10 @@ public: _jointIndex(jointIndex) { auto nestablePointer = _spatiallyNestable.lock(); if (nestablePointer) { - glm::vec3 nestableDimensions = getActualScale(nestablePointer); - _baseScale = glm::max(glm::vec3(0.001f), nestableDimensions); + if (nestablePointer->getNestableType() != NestableType::Avatar) { + glm::vec3 nestableDimensions = getActualScale(nestablePointer); + _baseScale = glm::max(glm::vec3(0.001f), nestableDimensions); + } } } From efc9f993f59db17556049aa9cdcfe40136fa2359 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 7 Mar 2019 17:39:19 -0800 Subject: [PATCH 032/117] Add FSTBaker, and make ModelBaker output an FST Restore feature to look for baked model file in other oven directory --- libraries/baking/src/Baker.h | 2 +- libraries/baking/src/FBXBaker.cpp | 11 ++ libraries/baking/src/FBXBaker.h | 3 +- libraries/baking/src/ModelBaker.cpp | 98 ++++++++++---- libraries/baking/src/ModelBaker.h | 15 ++- libraries/baking/src/baking/BakerLibrary.cpp | 15 ++- libraries/baking/src/baking/BakerLibrary.h | 9 +- libraries/baking/src/baking/FSTBaker.cpp | 128 +++++++++++++++++++ libraries/baking/src/baking/FSTBaker.h | 45 +++++++ libraries/fbx/src/FSTReader.h | 2 + tools/oven/src/DomainBaker.cpp | 20 +-- tools/oven/src/DomainBaker.h | 1 - 12 files changed, 301 insertions(+), 48 deletions(-) create mode 100644 libraries/baking/src/baking/FSTBaker.cpp create mode 100644 libraries/baking/src/baking/FSTBaker.h diff --git a/libraries/baking/src/Baker.h b/libraries/baking/src/Baker.h index c1b2ddf959..611f992c96 100644 --- a/libraries/baking/src/Baker.h +++ b/libraries/baking/src/Baker.h @@ -52,7 +52,7 @@ protected: void handleErrors(const QStringList& errors); // List of baked output files. For instance, for an FBX this would - // include the .fbx and all of its texture files. + // include the .fbx, a .fst pointing to the fbx, and all of the fbx texture files. std::vector _outputFiles; QStringList _errorList; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index e1bb86d051..d2dc86c783 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -37,6 +37,17 @@ #include "FBXToJSON.h" #endif +FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { + if (hasBeenBaked) { + // Look for the original model file one directory higher. Perhaps this is an oven output directory. + QUrl originalRelativePath = QUrl("../original/" + inputModelURL.fileName().replace(BAKED_FBX_EXTENSION, FBX_EXTENSION)); + QUrl newInputModelURL = inputModelURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath); + _modelURL = newInputModelURL; + } +} + void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { _hfmModel = hfmModel; // Load the root node from the FBX file diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 7770e3014d..f8a023f431 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -31,7 +31,8 @@ using TextureBakerThreadGetter = std::function; class FBXBaker : public ModelBaker { Q_OBJECT public: - using ModelBaker::ModelBaker; + FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); protected: virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 6568850c1f..c80df2db2e 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -21,6 +21,7 @@ #include #include +#include #ifdef _WIN32 #pragma warning( push ) @@ -61,12 +62,20 @@ ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter input qDebug() << "Made temporary dir " << _modelTempDir; qDebug() << "Origin file path: " << _originalModelFilePath; + { + auto bakedFilename = _modelURL.fileName(); + if (!hasBeenBaked) { + bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.')); + bakedFilename += BAKED_FBX_EXTENSION; + } + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; + } } ModelBaker::~ModelBaker() { if (_modelTempDir.exists()) { if (!_modelTempDir.remove(_originalModelFilePath)) { - qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath; + qCWarning(model_baking) << "Failed to remove temporary copy of model file:" << _originalModelFilePath; } if (!_modelTempDir.rmdir(".")) { qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir; @@ -74,6 +83,26 @@ ModelBaker::~ModelBaker() { } } +void ModelBaker::setOutputURLSuffix(const QUrl& outputURLSuffix) { + _outputURLSuffix = outputURLSuffix; +} + +void ModelBaker::setMappingURL(const QUrl& mappingURL) { + _mappingURL = mappingURL; +} + +void ModelBaker::setMapping(const hifi::VariantHash& mapping) { + _mapping = mapping; +} + +QUrl ModelBaker::getFullOutputMappingURL() const { + QUrl appendedURL = _outputMappingURL; + appendedURL.setFragment(_outputURLSuffix.fragment()); + appendedURL.setQuery(_outputURLSuffix.query()); + appendedURL.setUserInfo(_outputURLSuffix.userInfo()); + return appendedURL; +} + void ModelBaker::bake() { qDebug() << "ModelBaker" << _modelURL << "bake starting"; @@ -92,19 +121,24 @@ void ModelBaker::bake() { void ModelBaker::initializeOutputDirs() { // Attempt to make the output folders - // Warn if there is an output directory using the same name + // Warn if there is an output directory using the same name, unless we know a parent FST baker created them already if (QDir(_bakedOutputDir).exists()) { - qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + if (_mappingURL.isEmpty()) { + qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing."; + } } else { qCDebug(model_baking) << "Creating baked output folder" << _bakedOutputDir; if (!QDir().mkpath(_bakedOutputDir)) { handleError("Failed to create baked output folder " + _bakedOutputDir); + return; } } if (QDir(_originalOutputDir).exists()) { - qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; + if (_mappingURL.isEmpty()) { + qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; + } } else { qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir; if (!QDir().mkpath(_originalOutputDir)) { @@ -122,7 +156,7 @@ void ModelBaker::saveSourceModel() { qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; if (!localModelURL.exists()) { - //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); + //QMessageBox::warning(this, "Could not find " + _modelURL.toString(), ""); handleError("Could not find " + _modelURL.toString()); return; } @@ -135,7 +169,7 @@ void ModelBaker::saveSourceModel() { localModelURL.copy(_originalModelFilePath); - // emit our signal to start the import of the FBX source copy + // emit our signal to start the import of the model source copy emit modelLoaded(); } else { // remote file, kick off a download @@ -214,11 +248,11 @@ void ModelBaker::bakeSourceCopy() { handleError("Could not recognize file type of model file " + _originalModelFilePath); return; } - hifi::VariantHash mapping; - mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library - hfm::Model::Pointer loadedModel = serializer->read(modelData, mapping, _modelURL); + hifi::VariantHash serializerMapping = _mapping; + serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library + hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); - baker::Baker baker(loadedModel, mapping); + baker::Baker baker(loadedModel, serializerMapping); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation config->getJobConfig("BuildDracoMesh")->setEnabled(true); @@ -269,6 +303,32 @@ void ModelBaker::bakeSourceCopy() { return; } + // Output FST file, copying over input mappings if available + QString outputFSTFilename = !_mappingURL.isEmpty() ? _mappingURL.fileName() : _modelURL.fileName(); + auto extensionStart = outputFSTFilename.indexOf("."); + if (extensionStart != -1) { + outputFSTFilename.resize(extensionStart); + } + outputFSTFilename += ".baked.fst"; + QString outputFSTURL = _bakedOutputDir + "/" + outputFSTFilename; + + auto outputMapping = _mapping; + outputMapping[FST_VERSION_FIELD] = FST_VERSION; + outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); + hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); + + QFile fstOutputFile { outputFSTURL }; + if (!fstOutputFile.open(QIODevice::WriteOnly)) { + handleError("Failed to open file '" + outputFSTURL + "' for writing"); + return; + } + if (fstOutputFile.write(fstOut) == -1) { + handleError("Failed to write to file '" + outputFSTURL + "'"); + return; + } + _outputFiles.push_back(outputFSTURL); + _outputMappingURL = outputFSTURL; + // check if we're already done with textures (in case we had none to re-write) checkIfTexturesFinished(); } @@ -657,31 +717,25 @@ void ModelBaker::embedTextureMetaData() { } void ModelBaker::exportScene() { - // save the relative path to this FBX inside our passed output folder - auto fileName = _modelURL.fileName(); - auto baseName = fileName.left(fileName.lastIndexOf('.')); - auto bakedFilename = baseName + BAKED_FBX_EXTENSION; - - _bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename; - auto fbxData = FBXWriter::encodeFBX(_rootNode); - QFile bakedFile(_bakedModelFilePath); + QString bakedModelURL = _bakedModelURL.toString(); + QFile bakedFile(bakedModelURL); if (!bakedFile.open(QIODevice::WriteOnly)) { - handleError("Error opening " + _bakedModelFilePath + " for writing"); + handleError("Error opening " + bakedModelURL + " for writing"); return; } bakedFile.write(fbxData); - _outputFiles.push_back(_bakedModelFilePath); + _outputFiles.push_back(bakedModelURL); #ifdef HIFI_DUMP_FBX { FBXToJSON fbxToJSON; fbxToJSON << _rootNode; - QFileInfo modelFile(_bakedModelFilePath); + QFileInfo modelFile(_bakedModelURL.toString()); QString outFilename(modelFile.dir().absolutePath() + "/" + modelFile.completeBaseName() + "_FBX.json"); QFile jsonFile(outFilename); if (jsonFile.open(QIODevice::WriteOnly)) { @@ -691,5 +745,5 @@ void ModelBaker::exportScene() { } #endif - qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath; + qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << bakedModelURL; } diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index b0bd3798ff..f1ef6db56d 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -45,6 +45,10 @@ public: const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual ~ModelBaker(); + void setOutputURLSuffix(const QUrl& urlSuffix); + void setMappingURL(const QUrl& mappingURL); + void setMapping(const hifi::VariantHash& mapping); + void initializeOutputDirs(); bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); @@ -52,7 +56,8 @@ public: virtual void setWasAborted(bool wasAborted) override; QUrl getModelURL() const { return _modelURL; } - QString getBakedModelFilePath() const { return _bakedModelFilePath; } + virtual QUrl getFullOutputMappingURL() const; + QUrl getBakedModelURL() const { return _bakedModelURL; } signals: void modelLoaded(); @@ -72,9 +77,14 @@ protected: FBXNode _rootNode; QHash _textureContentMap; QUrl _modelURL; + QUrl _outputURLSuffix; + QUrl _mappingURL; + hifi::VariantHash _mapping; QString _bakedOutputDir; QString _originalOutputDir; - QString _bakedModelFilePath; + TextureBakerThreadGetter _textureThreadGetter; + QString _outputMappingURL; + QUrl _bakedModelURL; QDir _modelTempDir; QString _originalModelFilePath; @@ -93,7 +103,6 @@ private: const QString & bakedFilename, const QByteArray & textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - TextureBakerThreadGetter _textureThreadGetter; QMultiHash> _bakingTextures; QHash _textureNameMatchCount; QHash _remappedTexturePaths; diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index 202fd4b3d8..c95745146b 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -11,6 +11,7 @@ #include "BakerLibrary.h" +#include "FSTBaker.h" #include "../FBXBaker.h" #include "../OBJBaker.h" @@ -51,22 +52,24 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked"; QString originalOutputDirectory = contentOutputPath + subDirName + "/original"; + return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); +} + +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) { + auto filename = bakeableModelURL.fileName(); + std::unique_ptr baker; if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { - //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); + baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); - } else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { + //} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) { //baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory); } else { qDebug() << "Could not create ModelBaker for url" << bakeableModelURL; } - if (baker) { - QDir(contentOutputPath).mkpath(subDirName); - } - return baker; } diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index e77463b502..57197b53fd 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -16,13 +16,14 @@ #include "../ModelBaker.h" -// Returns either the given model URL, or, if the model is baked and shouldRebakeOriginals is true, -// the guessed location of the original model -// Returns an empty URL if no bakeable URL found +// Returns either the given model URL if valid, or an empty URL QUrl getBakeableModelURL(const QUrl& url); // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); -#endif hifi_BakerLibrary_h +// Similar to getModelBaker, but gives control over where the output folders will be +std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory); + +#endif // hifi_BakerLibrary_h diff --git a/libraries/baking/src/baking/FSTBaker.cpp b/libraries/baking/src/baking/FSTBaker.cpp new file mode 100644 index 0000000000..f76180bb58 --- /dev/null +++ b/libraries/baking/src/baking/FSTBaker.cpp @@ -0,0 +1,128 @@ +// +// FSTBaker.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/06. +// Copyright 2019 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 "FSTBaker.h" + +#include +#include + +#include "BakerLibrary.h" + +#include + +FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : + ModelBaker(inputMappingURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { + if (hasBeenBaked) { + // Look for the original model file one directory higher. Perhaps this is an oven output directory. + QUrl originalRelativePath = QUrl("../original/" + inputMappingURL.fileName().replace(BAKED_FST_EXTENSION, FST_EXTENSION)); + QUrl newInputMappingURL = inputMappingURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath); + _modelURL = newInputMappingURL; + } + _mappingURL = _modelURL; + + { + // Unused, but defined for consistency + auto bakedFilename = _modelURL.fileName(); + bakedFilename.replace(FST_EXTENSION, BAKED_FST_EXTENSION); + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; + } +} + +QUrl FSTBaker::getFullOutputMappingURL() const { + if (_modelBaker) { + return _modelBaker->getFullOutputMappingURL(); + } + return QUrl(); +} + +void FSTBaker::bakeSourceCopy() { + if (shouldStop()) { + return; + } + + QFile fstFile(_originalModelFilePath); + if (!fstFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalModelFilePath + " for reading"); + return; + } + + hifi::ByteArray fstData = fstFile.readAll(); + _mapping = FSTReader::readMapping(fstData); + + auto filenameField = _mapping[FILENAME_FIELD].toString(); + if (filenameField.isEmpty()) { + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be found"); + return; + } + auto modelURL = _mappingURL.adjusted(QUrl::RemoveFilename).resolved(filenameField); + auto bakeableModelURL = getBakeableModelURL(modelURL); + if (bakeableModelURL.isEmpty()) { + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be resolved to a valid bakeable model url"); + return; + } + + auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir); + _modelBaker = std::unique_ptr(dynamic_cast(baker.release())); + if (!_modelBaker) { + handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); + return; + } + if (dynamic_cast(_modelBaker.get())) { + // Could be interesting, but for now let's just prevent infinite FST loops in the most straightforward way possible + handleError("The FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported."); + return; + } + _modelBaker->setMappingURL(_mappingURL); + _modelBaker->setMapping(_mapping); + // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + _modelBaker->setOutputURLSuffix(modelURL); + + connect(_modelBaker.get(), &ModelBaker::aborted, this, &FSTBaker::handleModelBakerAborted); + connect(_modelBaker.get(), &ModelBaker::finished, this, &FSTBaker::handleModelBakerFinished); + + // FSTBaker can't do much while waiting for the ModelBaker to finish, so start the bake on this thread. + _modelBaker->bake(); +} + +void FSTBaker::handleModelBakerEnded() { + for (auto& warning : _modelBaker->getWarnings()) { + _warningList.push_back(warning); + } + for (auto& error : _modelBaker->getErrors()) { + _errorList.push_back(error); + } + + // Get the output files, including but not limited to the FST file and the baked model file + for (auto& outputFile : _modelBaker->getOutputFiles()) { + _outputFiles.push_back(outputFile); + } + +} + +void FSTBaker::handleModelBakerAborted() { + handleModelBakerEnded(); + if (!wasAborted()) { + setWasAborted(true); + } +} + +void FSTBaker::handleModelBakerFinished() { + handleModelBakerEnded(); + setIsFinished(true); +} + +void FSTBaker::abort() { + ModelBaker::abort(); + if (_modelBaker) { + _modelBaker->abort(); + } +} diff --git a/libraries/baking/src/baking/FSTBaker.h b/libraries/baking/src/baking/FSTBaker.h new file mode 100644 index 0000000000..aeb7286af3 --- /dev/null +++ b/libraries/baking/src/baking/FSTBaker.h @@ -0,0 +1,45 @@ +// +// FSTBaker.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/06. +// Copyright 2019 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_FSTBaker_h +#define hifi_FSTBaker_h + +#include "../ModelBaker.h" + +class FSTBaker : public ModelBaker { + Q_OBJECT + +public: + FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); + + virtual QUrl getFullOutputMappingURL() const; + +signals: + void fstLoaded(); + +public slots: + virtual void abort() override; + +protected: + std::unique_ptr _modelBaker; + +protected slots: + virtual void bakeSourceCopy() override; + virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override {}; + void handleModelBakerAborted(); + void handleModelBakerFinished(); + +private: + void handleModelBakerEnded(); +}; + +#endif // hifi_FSTBaker_h diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index ad952c4ed7..fade0fa5bc 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -15,6 +15,8 @@ #include #include +static const unsigned int FST_VERSION = 1; +static const QString FST_VERSION_FIELD = "version"; static const QString NAME_FIELD = "name"; static const QString TYPE_FIELD = "type"; static const QString FILENAME_FIELD = "filename"; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 42dfe59241..15b5a1ae12 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -152,8 +152,11 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); }; - QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &ModelBaker::deleteLater); + QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &Baker::deleteLater); if (baker) { + // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + baker->setOutputURLSuffix(url); + // make sure our handler is called when the baker is done connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); @@ -332,6 +335,7 @@ void DomainBaker::enumerateEntities() { addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { + // TODO: Do not combine mesh parts, otherwise the collision behavior will be different // TODO: this could be optimized so that we don't do the full baking pass for collision shapes, // but we have to handle the case where it's also used as a modelURL somewhere addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); @@ -415,11 +419,11 @@ void DomainBaker::handleFinishedModelBaker() { qDebug() << "Re-writing entity references to" << baker->getModelURL(); // setup a new URL using the prefix we were passed - auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); - if (relativeFBXFilePath.startsWith("/")) { - relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); + auto relativeMappingFilePath = baker->getFullOutputMappingURL().toString().remove(_contentOutputPath); + if (relativeMappingFilePath.startsWith("/")) { + relativeMappingFilePath = relativeMappingFilePath.right(relativeMappingFilePath.length() - 1); } - QUrl newURL = _destinationPath.resolved(relativeFBXFilePath); + QUrl newURL = _destinationPath.resolved(relativeMappingFilePath); // enumerate the QJsonRef values for the URL of this model from our multi hash of // entity objects needing a URL re-write @@ -432,12 +436,8 @@ void DomainBaker::handleFinishedModelBaker() { // grab the old URL QUrl oldURL = entity[property].toString(); - // copy the fragment and query, and user info from the old model URL - newURL.setQuery(oldURL.query()); - newURL.setFragment(oldURL.fragment()); - newURL.setUserInfo(oldURL.userInfo()); - // set the new URL as the value in our temp QJsonObject + // The fragment, query, and user info from the original model URL should now be present on the filename in the FST file entity[property] = newURL.toString(); } else { // Group property diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 4504d5b8fa..dbbf182fa7 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -17,7 +17,6 @@ #include #include -#include "Baker.h" #include "ModelBaker.h" #include "TextureBaker.h" #include "JSBaker.h" From 5b504c47590032a474a125c97fa7c3186fdb05db Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 4 Mar 2019 18:04:40 -0800 Subject: [PATCH 033/117] Add encode/decode speed config to BuildDracoMeshTask --- libraries/baking/src/ModelBaker.cpp | 10 ---------- .../model-baker/src/model-baker/BuildDracoMeshTask.cpp | 5 +++-- .../model-baker/src/model-baker/BuildDracoMeshTask.h | 9 +++++++++ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index c80df2db2e..f3954c98da 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -227,9 +227,6 @@ void ModelBaker::handleModelNetworkReply() { } } -// TODO: Remove after testing -#include - void ModelBaker::bakeSourceCopy() { QFile modelFile(_originalModelFilePath); if (!modelFile.open(QIODevice::ReadOnly)) { @@ -258,13 +255,6 @@ void ModelBaker::bakeSourceCopy() { config->getJobConfig("BuildDracoMesh")->setEnabled(true); // Do not permit potentially lossy modification of joint data meant for runtime ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; - - // TODO: Remove after testing - { - auto* dracoConfig = ((BuildDracoMeshConfig*)config->getJobConfig("BuildDracoMesh")); - dracoConfig->encodeSpeed = 10; - dracoConfig->decodeSpeed = -1; - } // Begin hfm baking baker.run(); diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 9bfd03e218..e45b2bf584 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -193,7 +193,8 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v } void BuildDracoMeshTask::configure(const Config& config) { - // Nothing to configure yet + _encodeSpeed = config.encodeSpeed; + _decodeSpeed = config.decodeSpeed; } void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { @@ -222,7 +223,7 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); - encoder.SetSpeedOptions(0, 5); + encoder.SetSpeedOptions(_encodeSpeed, _decodeSpeed); draco::EncoderBuffer buffer; encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h index ab1679959a..0e33be3c41 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -21,8 +21,13 @@ // BuildDracoMeshTask is disabled by default class BuildDracoMeshConfig : public baker::JobConfig { Q_OBJECT + Q_PROPERTY(int encodeSpeed MEMBER encodeSpeed) + Q_PROPERTY(int decodeSpeed MEMBER decodeSpeed) public: BuildDracoMeshConfig() : baker::JobConfig(false) {} + + int encodeSpeed { 0 }; + int decodeSpeed { 5 }; }; class BuildDracoMeshTask { @@ -34,6 +39,10 @@ public: void configure(const Config& config); void run(const baker::BakeContextPointer& context, const Input& input, Output& output); + +protected: + int _encodeSpeed { 0 }; + int _decodeSpeed { 5 }; }; #endif // hifi_BuildDracoMeshTask_h From b42c6d1352d815699d10c9af9da0eaf6ba59a171 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 11:24:56 -0700 Subject: [PATCH 034/117] Fix baked models not mapping to correct textures --- libraries/baking/src/ModelBaker.cpp | 5 +++++ libraries/baking/src/TextureBaker.cpp | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index f3954c98da..ac69653e45 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -305,6 +305,8 @@ void ModelBaker::bakeSourceCopy() { auto outputMapping = _mapping; outputMapping[FST_VERSION_FIELD] = FST_VERSION; outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); + // All textures will be found in the same directory as the model + outputMapping[TEXDIR_FIELD] = "."; hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); QFile fstOutputFile { outputFSTURL }; @@ -403,6 +405,9 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture // ensuring that the baked texture will have a unique name // even if there was another texture with the same name at a different path baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo); + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); + baseTextureFileName += addMapChannel; _remappedTexturePaths[urlToTexture] = baseTextureFileName; } diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index db54cbdf98..8591cbd0aa 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -128,11 +128,8 @@ void TextureBaker::processTexture() { TextureMeta meta; - // If two textures have the same URL but are used differently, we need to process them separately - QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); - _baseFilename += addMapChannel; - QString newFilename = _textureURL.fileName(); + QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); newFilename.replace(QString("."), addMapChannel + "."); QString originalCopyFilePath = _outputDirectory.absoluteFilePath(newFilename); From abbbeb11e1e85395e50f6da4c4830f030347dde0 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 11:59:21 -0700 Subject: [PATCH 035/117] Comment out baking for collision model, with explanation --- tools/oven/src/DomainBaker.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 15b5a1ae12..28b45eccaf 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -335,10 +335,12 @@ void DomainBaker::enumerateEntities() { addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it); } if (entity.contains(COMPOUND_SHAPE_URL_KEY)) { - // TODO: Do not combine mesh parts, otherwise the collision behavior will be different + // TODO: Support collision model baking + // Do not combine mesh parts, otherwise the collision behavior will be different + // combineParts is currently only used by OBJBaker (mesh-combining functionality ought to be moved to the asset engine at some point), and is also used by OBJBaker to determine if the material library should be loaded (should be separate flag) // TODO: this could be optimized so that we don't do the full baking pass for collision shapes, // but we have to handle the case where it's also used as a modelURL somewhere - addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); + //addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it); } if (entity.contains(ANIMATION_KEY)) { auto animationObject = entity[ANIMATION_KEY].toObject(); From 09c30269d4e78ec492e8f54ed3a4cca783d7dd80 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 12:44:21 -0700 Subject: [PATCH 036/117] Add a note about FST support for url userinfo/query/fragment --- tools/oven/src/DomainBaker.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 28b45eccaf..0f07a392b4 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -155,6 +155,10 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs QSharedPointer baker = QSharedPointer(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &Baker::deleteLater); if (baker) { // Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL + // Note: The ModelBaker currently doesn't store this in the FST because the equal signs mess up FST parsing. + // There is a small chance this could break a server workflow relying on the old behavior. + // Url suffix is still propagated to the baked URL if the input URL is an FST. + // Url suffix has always been stripped from the URL when loading the original model file to be baked. baker->setOutputURLSuffix(url); // make sure our handler is called when the baker is done From 41c05943612660725bc246af63033a557cfc5e2b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 13:27:39 -0700 Subject: [PATCH 037/117] Make output folder cleaner for single model bake when baked model url is given as input --- libraries/baking/src/baking/BakerLibrary.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index c95745146b..c516338ad1 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -42,7 +42,7 @@ std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureB auto filename = bakeableModelURL.fileName(); // Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique - auto baseName = filename.left(filename.lastIndexOf('.')); + auto baseName = filename.left(filename.lastIndexOf('.')).left(filename.lastIndexOf(".baked")); auto subDirName = "/" + baseName; int i = 1; while (QDir(contentOutputPath + subDirName).exists()) { From f9f55f08dbd8009f81f807926a89b0d6f3525e04 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 13:46:28 -0700 Subject: [PATCH 038/117] Fix model baking GUI being less lenient with local Windows files --- tools/oven/src/ui/ModelBakeWidget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 8f8e068b50..79ab733b0c 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -176,9 +176,9 @@ void ModelBakeWidget::bakeButtonClicked() { auto fileURLStrings = _modelLineEdit->text().split(','); foreach (QString fileURLString, fileURLStrings) { // construct a URL from the path in the model file text box - QUrl modelToBakeURL(fileURLString); + QUrl modelToBakeURL = QUrl::fromUserInput(fileURLString); - QUrl bakeableModelURL = getBakeableModelURL(QUrl(modelToBakeURL)); + QUrl bakeableModelURL = getBakeableModelURL(modelToBakeURL); if (!bakeableModelURL.isEmpty()) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); From 06d38bf8e73c2f06acb531cfbd4d7295f7377a9b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 15:25:12 -0700 Subject: [PATCH 039/117] Fix DomainBaker not outputting URLs relative to the Destination URL Path --- tools/oven/src/DomainBaker.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 0f07a392b4..109d2f1809 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -425,10 +425,11 @@ void DomainBaker::handleFinishedModelBaker() { qDebug() << "Re-writing entity references to" << baker->getModelURL(); // setup a new URL using the prefix we were passed - auto relativeMappingFilePath = baker->getFullOutputMappingURL().toString().remove(_contentOutputPath); + auto relativeMappingFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getFullOutputMappingURL().toString()); if (relativeMappingFilePath.startsWith("/")) { relativeMappingFilePath = relativeMappingFilePath.right(relativeMappingFilePath.length() - 1); } + QUrl newURL = _destinationPath.resolved(relativeMappingFilePath); // enumerate the QJsonRef values for the URL of this model from our multi hash of @@ -494,7 +495,12 @@ void DomainBaker::handleFinishedTextureBaker() { // this TextureBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getTextureURL(); - auto newURL = _destinationPath.resolved(baker->getMetaTextureFileName()); + // setup a new URL using the prefix we were passed + auto relativeTextureFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getMetaTextureFileName()); + if (relativeTextureFilePath.startsWith("/")) { + relativeTextureFilePath = relativeTextureFilePath.right(relativeTextureFilePath.length() - 1); + } + auto newURL = _destinationPath.resolved(relativeTextureFilePath); // enumerate the QJsonRef values for the URL of this texture from our multi hash of // entity objects needing a URL re-write @@ -563,7 +569,12 @@ void DomainBaker::handleFinishedScriptBaker() { // this JSBaker is done and everything went according to plan qDebug() << "Re-writing entity references to" << baker->getJSPath(); - auto newURL = _destinationPath.resolved(baker->getBakedJSFilePath()); + // setup a new URL using the prefix we were passed + auto relativeScriptFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedJSFilePath()); + if (relativeScriptFilePath.startsWith("/")) { + relativeScriptFilePath = relativeScriptFilePath.right(relativeScriptFilePath.length() - 1); + } + auto newURL = _destinationPath.resolved(relativeScriptFilePath); // enumerate the QJsonRef values for the URL of this script from our multi hash of // entity objects needing a URL re-write @@ -634,7 +645,12 @@ void DomainBaker::handleFinishedMaterialBaker() { QString newDataOrURL; if (baker->isURL()) { - newDataOrURL = _destinationPath.resolved(baker->getBakedMaterialData()).toDisplayString(); + // setup a new URL using the prefix we were passed + auto relativeMaterialFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedMaterialData()); + if (relativeMaterialFilePath.startsWith("/")) { + relativeMaterialFilePath = relativeMaterialFilePath.right(relativeMaterialFilePath.length() - 1); + } + newDataOrURL = _destinationPath.resolved(relativeMaterialFilePath).toDisplayString(); } else { newDataOrURL = baker->getBakedMaterialData(); } From 1e354fb280b3e42f7698edddf16ebd003139b5b6 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Tue, 12 Mar 2019 17:18:15 -0700 Subject: [PATCH 040/117] fix notications scaling --- scripts/system/notifications.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 1d6b4dada3..469f30cd23 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -203,7 +203,7 @@ // Notification plane positions noticeY = -sensorScaleFactor * (y * NOTIFICATION_3D_SCALE + 0.5 * noticeHeight); notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: 0.5 * sensorScaleFactor * (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH), y: noticeY, z: 0.001 }; + buttonPosition = { x: sensorScaleFactor * (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH), y: noticeY, z: 0.001 }; // Rotate plane notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, @@ -241,7 +241,7 @@ noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - notice.size = { x: noticeWidth, y: noticeHeight }; + notice.size = { x: noticeWidth * sensorScaleFactor, y: noticeHeight * sensorScaleFactor }; positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); @@ -249,15 +249,15 @@ notice.parentJointIndex = -2; if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE * sensorScaleFactor; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE * sensorScaleFactor; notice.bottomMargin = 0; notice.rightMargin = 0; notice.lineHeight = 10.0 * (fontSize * sensorScaleFactor / 12.0) * NOTIFICATION_3D_SCALE; notice.isFacingAvatar = false; notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); + notifications.push(notificationText); } else { notifications.push(Overlays.addOverlay("image3d", notice)); } @@ -267,14 +267,15 @@ button.isFacingAvatar = false; button.parentID = MyAvatar.sessionUUID; button.parentJointIndex = -2; + button.visible = false; buttons.push((Overlays.addOverlay("image3d", button))); overlay3DDetails.push({ notificationOrientation: positions.notificationOrientation, notificationPosition: positions.notificationPosition, buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight + width: noticeWidth * sensorScaleFactor, + height: noticeHeight * sensorScaleFactor }); From a93825c2f96133d1ef09bbe2db8e6dab8ddf0851 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 18:11:33 -0700 Subject: [PATCH 041/117] Fix remaining issues with merge --- libraries/baking/src/ModelBaker.cpp | 2 +- libraries/model-baker/src/model-baker/Baker.cpp | 9 ++++++--- libraries/model-baker/src/model-baker/Baker.h | 2 +- libraries/model-baker/src/model-baker/BakerTypes.h | 1 - .../src/model-baker/ParseMaterialMappingTask.cpp | 4 ++-- .../src/model-baker/ParseMaterialMappingTask.h | 4 +++- .../model-networking/src/model-networking/ModelCache.cpp | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index ac69653e45..d38b965f6d 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -249,7 +249,7 @@ void ModelBaker::bakeSourceCopy() { serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); - baker::Baker baker(loadedModel, serializerMapping); + baker::Baker baker(loadedModel, serializerMapping, hifi::URL()); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation config->getJobConfig("BuildDracoMesh")->setEnabled(true); diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 113c1a0fe5..7bb53376ed 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -119,12 +119,13 @@ namespace baker { class BakerEngineBuilder { public: - using Input = VaryingSet2; + using Input = VaryingSet3; using Output = VaryingSet4, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); const auto& mapping = input.getN(1); + const auto& materialMappingBaseURL = input.getN(2); // Split up the inputs from hfm::Model const auto modelPartsIn = model.addJob("GetModelParts", hfmModelIn); @@ -157,7 +158,8 @@ namespace baker { const auto jointIndices = jointInfoOut.getN(2); // Parse material mapping - const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); + const auto parseMaterialMappingInputs = ParseMaterialMappingTask::Input(mapping, materialMappingBaseURL).asVarying(); + const auto materialMapping = model.addJob("ParseMaterialMapping", parseMaterialMappingInputs); // Build Draco meshes // NOTE: This task is disabled by default and must be enabled through configuration @@ -182,10 +184,11 @@ namespace baker { } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { _engine->feedInput(0, hfmModel); _engine->feedInput(1, mapping); + _engine->feedInput(2, materialMappingBaseURL); } std::shared_ptr Baker::getConfiguration() { diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 056431dc8e..6f74cb646e 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -23,7 +23,7 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping); + Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL); std::shared_ptr getConfiguration(); diff --git a/libraries/model-baker/src/model-baker/BakerTypes.h b/libraries/model-baker/src/model-baker/BakerTypes.h index 8b80b0bde4..3d16afab2e 100644 --- a/libraries/model-baker/src/model-baker/BakerTypes.h +++ b/libraries/model-baker/src/model-baker/BakerTypes.h @@ -36,7 +36,6 @@ namespace baker { using TangentsPerBlendshape = std::vector>; using MeshIndicesToModelNames = QHash; - using GeometryMappingPair = std::pair; }; #endif // hifi_BakerTypes_h diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index 0a1964d8cd..acb2bdc1c5 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -11,8 +11,8 @@ #include "ModelBakerLogging.h" void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { - const auto& url = input.first; - const auto& mapping = input.second; + const auto& mapping = input.get0(); + const auto& url = input.get1(); MaterialMapping materialMapping; auto mappingIter = mapping.find("materialMap"); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h index 5f5eff327d..7c94661b28 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -13,6 +13,8 @@ #include +#include + #include "Engine.h" #include "BakerTypes.h" @@ -20,7 +22,7 @@ class ParseMaterialMappingTask { public: - using Input = baker::GeometryMappingPair; + using Input = baker::VaryingSet2; using Output = MaterialMapping; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 364bf010a6..12553b42eb 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -340,7 +340,7 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) { // Do processing on the model - baker::Baker modelBaker(hfmModel, mapping.second); + baker::Baker modelBaker(hfmModel, mapping.second, mapping.first); modelBaker.run(); // Assume ownership of the processed HFMModel From 7f77e163acd3570d4edc228d4cd613374128e785 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 18:17:11 -0700 Subject: [PATCH 042/117] Restore 'Re-bake originals' checkbox to domain bake window --- libraries/baking/src/baking/BakerLibrary.cpp | 8 ++++++++ libraries/baking/src/baking/BakerLibrary.h | 2 ++ tools/oven/src/DomainBaker.cpp | 8 +++++--- tools/oven/src/DomainBaker.h | 5 ++++- tools/oven/src/ui/DomainBakeWidget.cpp | 7 ++++++- tools/oven/src/ui/DomainBakeWidget.h | 1 + 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/libraries/baking/src/baking/BakerLibrary.cpp b/libraries/baking/src/baking/BakerLibrary.cpp index c516338ad1..2afeef4800 100644 --- a/libraries/baking/src/baking/BakerLibrary.cpp +++ b/libraries/baking/src/baking/BakerLibrary.cpp @@ -38,6 +38,13 @@ QUrl getBakeableModelURL(const QUrl& url) { return QUrl(); } +bool isModelBaked(const QUrl& bakeableModelURL) { + auto modelString = bakeableModelURL.toString(); + auto beforeModelExtension = modelString; + beforeModelExtension.resize(modelString.lastIndexOf('.')); + return beforeModelExtension.endsWith(".baked"); +} + std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) { auto filename = bakeableModelURL.fileName(); @@ -59,6 +66,7 @@ std::unique_ptr getModelBakerWithOutputDirectories(const QUrl& bakea auto filename = bakeableModelURL.fileName(); std::unique_ptr baker; + if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) { baker = std::make_unique(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive)); } else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) { diff --git a/libraries/baking/src/baking/BakerLibrary.h b/libraries/baking/src/baking/BakerLibrary.h index 57197b53fd..a646c8d36a 100644 --- a/libraries/baking/src/baking/BakerLibrary.h +++ b/libraries/baking/src/baking/BakerLibrary.h @@ -19,6 +19,8 @@ // Returns either the given model URL if valid, or an empty URL QUrl getBakeableModelURL(const QUrl& url); +bool isModelBaked(const QUrl& bakeableModelURL); + // Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored // Returns an empty pointer if a baker could not be created std::unique_ptr getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath); diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 109d2f1809..5f8ec3a678 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -23,10 +23,12 @@ #include "baking/BakerLibrary.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, - const QString& baseOutputPath, const QUrl& destinationPath) : + const QString& baseOutputPath, const QUrl& destinationPath, + bool shouldRebakeOriginals) : _localEntitiesFileURL(localModelFileURL), _domainName(domainName), - _baseOutputPath(baseOutputPath) + _baseOutputPath(baseOutputPath), + _shouldRebakeOriginals(shouldRebakeOriginals) { // make sure the destination path has a trailing slash if (!destinationPath.toString().endsWith('/')) { @@ -146,7 +148,7 @@ void DomainBaker::loadLocalFile() { void DomainBaker::addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { // grab a QUrl for the model URL QUrl bakeableModelURL = getBakeableModelURL(url); - if (!bakeableModelURL.isEmpty()) { + if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) { // setup a ModelBaker for this URL, as long as we don't already have one if (!_modelBakers.contains(bakeableModelURL)) { auto getWorkerThreadCallback = []() -> QThread* { diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index dbbf182fa7..c9f5a59672 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -29,7 +29,8 @@ public: // 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); + const QString& baseOutputPath, const QUrl& destinationPath, + bool shouldRebakeOriginals); signals: void allModelsFinished(); @@ -70,6 +71,8 @@ private: int _totalNumberOfSubBakes { 0 }; int _completedSubBakes { 0 }; + bool _shouldRebakeOriginals { false }; + void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); diff --git a/tools/oven/src/ui/DomainBakeWidget.cpp b/tools/oven/src/ui/DomainBakeWidget.cpp index 23074e775e..1121041e39 100644 --- a/tools/oven/src/ui/DomainBakeWidget.cpp +++ b/tools/oven/src/ui/DomainBakeWidget.cpp @@ -126,6 +126,10 @@ void DomainBakeWidget::setupUI() { // start a new row for the next component ++rowIndex; + // setup a checkbox to allow re-baking of original assets + _rebakeOriginalsCheckBox = new QCheckBox("Re-bake originals"); + gridLayout->addWidget(_rebakeOriginalsCheckBox, rowIndex, 0); + // add a button that will kickoff the bake QPushButton* bakeButton = new QPushButton("Bake"); connect(bakeButton, &QPushButton::clicked, this, &DomainBakeWidget::bakeButtonClicked); @@ -207,7 +211,8 @@ void DomainBakeWidget::bakeButtonClicked() { auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text()); auto domainBaker = std::unique_ptr { new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(), - outputDirectory.absolutePath(), _destinationPathLineEdit->text()) + outputDirectory.absolutePath(), _destinationPathLineEdit->text(), + _rebakeOriginalsCheckBox->isChecked()) }; // make sure we hear from the baker when it is done diff --git a/tools/oven/src/ui/DomainBakeWidget.h b/tools/oven/src/ui/DomainBakeWidget.h index 0a1d613912..a6f26b3731 100644 --- a/tools/oven/src/ui/DomainBakeWidget.h +++ b/tools/oven/src/ui/DomainBakeWidget.h @@ -45,6 +45,7 @@ private: QLineEdit* _entitiesFileLineEdit; QLineEdit* _outputDirLineEdit; QLineEdit* _destinationPathLineEdit; + QCheckBox* _rebakeOriginalsCheckBox; Setting::Handle _domainNameSetting; Setting::Handle _exportDirectory; From b35b7687b727d1c69cc9cc943b666cc66d9276df Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 18:32:17 -0700 Subject: [PATCH 043/117] Ready. --- tools/nitpick/src/AWSInterface.cpp | 33 ++++++++++++++++++++++++------ tools/nitpick/src/AWSInterface.h | 11 ++++++++-- tools/nitpick/src/Nitpick.cpp | 4 ++-- tools/nitpick/src/TestCreator.cpp | 9 ++++++-- tools/nitpick/src/TestCreator.h | 5 ++++- tools/nitpick/src/common.h | 1 + 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index a098d17917..16c0a220d8 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -8,6 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "AWSInterface.h" +#include "common.h" #include #include @@ -29,7 +30,9 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user ) { _workingDirectory = workingDirectory; @@ -53,6 +56,13 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, _urlLineEdit = urlLineEdit; _urlLineEdit->setEnabled(false); + + _urlLineEdit = urlLineEdit; + _urlLineEdit->setEnabled(false); + + _branch = branch; + _user = user; + QString zipFilenameWithoutExtension = zipFilename.split('.')[0]; extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension); @@ -202,13 +212,21 @@ void AWSInterface::writeTitle(QTextStream& stream, const QStringList& originalNa stream << "run on " << hostName << "\n"; - int numberOfFailures = originalNamesFailures.length(); - int numberOfSuccesses = originalNamesSuccesses.length(); + stream << "

"; + stream << "nitpick " << nitpickVersion; + stream << ", tests from GitHub: " << _user << "/" << _branch; + stream << "

"; - stream << "

" << QString::number(numberOfFailures) << " failed, out of a total of " << QString::number(numberOfSuccesses) << " tests

\n"; + _numberOfFailures = originalNamesFailures.length(); + _numberOfSuccesses = originalNamesSuccesses.length(); + + stream << "

" << QString::number(_numberOfFailures) << " failed, out of a total of " << QString::number(_numberOfFailures + _numberOfSuccesses) << " tests

\n"; stream << "\t" << "\t" << "\n"; - stream << "\t" << "\t" << "

The following tests failed:

"; + + if (_numberOfFailures > 0) { + stream << "\t" << "\t" << "

The following tests failed:

"; + } } void AWSInterface::writeTable(QTextStream& stream, const QStringList& originalNamesFailures, const QStringList& originalNamesSuccesses) { @@ -289,7 +307,10 @@ void AWSInterface::writeTable(QTextStream& stream, const QStringList& originalNa closeTable(stream); stream << "\t" << "\t" << "\n"; - stream << "\t" << "\t" << "

The following tests passed:

"; + + if (_numberOfSuccesses > 0) { + stream << "\t" << "\t" << "

The following tests passed:

"; + } // Now do the same for passes folderNames.clear(); diff --git a/tools/nitpick/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h index 77d500fa7c..a2e4e36c37 100644 --- a/tools/nitpick/src/AWSInterface.h +++ b/tools/nitpick/src/AWSInterface.h @@ -31,7 +31,10 @@ public: QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit); + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ); void extractTestFailuresFromZippedFolder(const QString& folderName); void createHTMLFile(); @@ -70,9 +73,13 @@ private: QString AWS_BUCKET{ "hifi-qa" }; QLineEdit* _urlLineEdit; - + QString _user; + QString _branch; QString _comparisonImageFilename; + + int _numberOfFailures; + int _numberOfSuccesses; }; #endif // hifi_AWSInterface_h \ No newline at end of file diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..02ed120350 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - " + nitpickVersion); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); @@ -266,7 +266,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() { } void Nitpick::on_createWebPagePushbutton_clicked() { - _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit); + _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit, _ui.branchLineEdit->text(), _ui.userLineEdit->text()); } void Nitpick::about() { diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..f45a23e459 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -1112,7 +1112,10 @@ void TestCreator::createWebPage( QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ) { QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, "Zipped TestCreator Results (TestResults--*.zip)"); @@ -1136,6 +1139,8 @@ void TestCreator::createWebPage( updateAWSCheckBox, diffImageRadioButton, ssimImageRadionButton, - urlLineEdit + urlLineEdit, + branch, + user ); } \ No newline at end of file diff --git a/tools/nitpick/src/TestCreator.h b/tools/nitpick/src/TestCreator.h index 7cd38b42d4..50aa06e944 100644 --- a/tools/nitpick/src/TestCreator.h +++ b/tools/nitpick/src/TestCreator.h @@ -107,7 +107,10 @@ public: QCheckBox* updateAWSCheckBox, QRadioButton* diffImageRadioButton, QRadioButton* ssimImageRadionButton, - QLineEdit* urlLineEdit); + QLineEdit* urlLineEdit, + const QString& branch, + const QString& user + ); private: QProgressBar* _progressBar; diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index eb228ff2b3..b0a58747c1 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -56,4 +56,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; +const QString nitpickVersion { "v3.1.4" }; #endif // hifi_common_h \ No newline at end of file From b855b0e2a30442a4cfab5a179949dafde32faf49 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Tue, 12 Mar 2019 18:33:41 -0700 Subject: [PATCH 044/117] Document available bake types for baker command line --- tools/oven/src/OvenCLIApplication.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/oven/src/OvenCLIApplication.cpp b/tools/oven/src/OvenCLIApplication.cpp index c405c5f4a0..b4a011291d 100644 --- a/tools/oven/src/OvenCLIApplication.cpp +++ b/tools/oven/src/OvenCLIApplication.cpp @@ -33,7 +33,7 @@ OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) : parser.addOptions({ { CLI_INPUT_PARAMETER, "Path to file that you would like to bake.", "input" }, { CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" }, - { CLI_TYPE_PARAMETER, "Type of asset.", "type" }, + { CLI_TYPE_PARAMETER, "Type of asset. [model|material|js]", "type" }, { CLI_DISABLE_TEXTURE_COMPRESSION_PARAMETER, "Disable texture compression." } }); From 609c4ab52ea5b138f0e41e99212cc63c6e9fcec2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Mar 2019 18:41:43 -0700 Subject: [PATCH 045/117] try to fix audio injector threading issues --- interface/resources/qml/Stats.qml | 4 + interface/src/Application.cpp | 9 +- interface/src/avatar/AvatarManager.cpp | 5 +- interface/src/avatar/AvatarManager.h | 4 +- .../src/scripting/TTSScriptingInterface.cpp | 10 +- interface/src/ui/Keyboard.cpp | 4 +- interface/src/ui/Keyboard.h | 2 +- interface/src/ui/Stats.cpp | 5 + interface/src/ui/Stats.h | 9 + libraries/audio-client/src/AudioClient.cpp | 43 +-- libraries/audio-client/src/AudioClient.h | 2 + libraries/audio/src/AudioInjector.cpp | 267 +++++------------- libraries/audio/src/AudioInjector.h | 47 ++- .../audio/src/AudioInjectorLocalBuffer.cpp | 7 +- .../audio/src/AudioInjectorLocalBuffer.h | 1 + libraries/audio/src/AudioInjectorManager.cpp | 229 +++++++++++++-- libraries/audio/src/AudioInjectorManager.h | 22 +- .../src/EntityTreeRenderer.cpp | 2 +- .../src/EntityTreeRenderer.h | 2 +- .../src/AudioScriptingInterface.cpp | 2 +- .../script-engine/src/ScriptAudioInjector.cpp | 19 +- .../script-engine/src/ScriptAudioInjector.h | 21 +- .../ui/src/ui/TabletScriptingInterface.cpp | 4 +- libraries/ui/src/ui/types/SoundEffect.cpp | 11 +- libraries/ui/src/ui/types/SoundEffect.h | 6 +- 25 files changed, 403 insertions(+), 334 deletions(-) diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 6748418d19..e10f86a947 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -232,6 +232,10 @@ Item { text: "Audio Codec: " + root.audioCodec + " Noise Gate: " + root.audioNoiseGate; } + StatText { + visible: root.expanded; + text: "Injectors (Local/NonLocal): " + root.audioInjectors.x + "/" + root.audioInjectors.y; + } StatText { visible: root.expanded; text: "Entity Servers In: " + root.entityPacketsInKbps + " kbps"; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6d9a1823a1..fd8f8dd4b0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2695,9 +2695,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); - if (_snapshotSoundInjector != nullptr) { - _snapshotSoundInjector->stop(); - } + _snapshotSoundInjector = nullptr; // destroy Audio so it and its threads have a chance to go down safely // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine @@ -4216,10 +4214,9 @@ void Application::keyPressEvent(QKeyEvent* event) { Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; if (notificationSounds.get() && notificationSoundSnapshot.get()) { if (_snapshotSoundInjector) { - _snapshotSoundInjector->setOptions(options); - _snapshotSoundInjector->restart(); + DependencyManager::get()->setOptionsAndRestart(_snapshotSoundInjector, options); } else { - _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); + _snapshotSoundInjector = DependencyManager::get()->playSound(_snapshotSound, options); } } takeSnapshot(true); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c66c0a30cb..69f7054953 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -629,8 +629,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // but most avatars are roughly the same size, so let's not be so fancy yet. const float AVATAR_STRETCH_FACTOR = 1.0f; - _collisionInjectors.remove_if( - [](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); }); + _collisionInjectors.remove_if([](const AudioInjectorPointer& injector) { return !injector; }); static const int MAX_INJECTOR_COUNT = 3; if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) { @@ -640,7 +639,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents options.volume = energyFactorOfFull; options.pitch = 1.0f / AVATAR_STRETCH_FACTOR; - auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); + auto injector = DependencyManager::get()->playSound(collisionSound, options, true); _collisionInjectors.emplace_back(injector); } } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 2b58b14d11..0468fbd809 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include // for SetOfEntities @@ -239,7 +239,7 @@ private: std::shared_ptr _myAvatar; quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate. - std::list _collisionInjectors; + std::list> _collisionInjectors; RateCounter<> _myAvatarSendRate; int _numAvatarsUpdated { 0 }; diff --git a/interface/src/scripting/TTSScriptingInterface.cpp b/interface/src/scripting/TTSScriptingInterface.cpp index 6589769ece..325e1ff649 100644 --- a/interface/src/scripting/TTSScriptingInterface.cpp +++ b/interface/src/scripting/TTSScriptingInterface.cpp @@ -66,7 +66,7 @@ void TTSScriptingInterface::updateLastSoundAudioInjector() { if (_lastSoundAudioInjector) { AudioInjectorOptions options; options.position = DependencyManager::get()->getMyAvatarPosition(); - _lastSoundAudioInjector->setOptions(options); + DependencyManager::get()->setOptions(_lastSoundAudioInjector, options); _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); } } @@ -143,7 +143,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { options.position = DependencyManager::get()->getMyAvatarPosition(); if (_lastSoundAudioInjector) { - _lastSoundAudioInjector->stop(); + DependencyManager::get()->stop(_lastSoundAudioInjector); _lastSoundAudioInjectorUpdateTimer.stop(); } @@ -151,7 +151,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { uint32_t numSamples = (uint32_t)_lastSoundByteArray.size() / sizeof(AudioData::AudioSample); auto samples = reinterpret_cast(_lastSoundByteArray.data()); auto newAudioData = AudioData::make(numSamples, numChannels, samples); - _lastSoundAudioInjector = AudioInjector::playSoundAndDelete(newAudioData, options); + _lastSoundAudioInjector = DependencyManager::get()->playSound(newAudioData, options, true); _lastSoundAudioInjectorUpdateTimer.start(INJECTOR_INTERVAL_MS); #else @@ -161,7 +161,7 @@ void TTSScriptingInterface::speakText(const QString& textToSpeak) { void TTSScriptingInterface::stopLastSpeech() { if (_lastSoundAudioInjector) { - _lastSoundAudioInjector->stop(); - _lastSoundAudioInjector = NULL; + DependencyManager::get()->stop(_lastSoundAudioInjector); + _lastSoundAudioInjector = nullptr; } } diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index 9b75f78e67..1cbe31f1eb 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include #include @@ -537,7 +537,7 @@ void Keyboard::handleTriggerBegin(const QUuid& id, const PointerEvent& event) { audioOptions.position = keyWorldPosition; audioOptions.volume = 0.05f; - AudioInjector::playSoundAndDelete(_keySound, audioOptions); + DependencyManager::get()->playSound(_keySound, audioOptions, true); int scanCode = key.getScanCode(_capsEnabled); QString keyString = key.getKeyString(_capsEnabled); diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index b3358e486d..51e5e0571f 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -19,9 +19,9 @@ #include #include #include +#include #include #include -#include #include #include diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index ecdae0b375..3c943028f5 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -266,6 +266,11 @@ void Stats::updateStats(bool force) { } STAT_UPDATE(audioCodec, audioClient->getSelectedAudioFormat()); STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); + { + int localInjectors = audioClient->getNumLocalInjectors(); + int nonLocalInjectors = DependencyManager::get()->getNumInjectors(); + STAT_UPDATE(audioInjectors, QVector2D(localInjectors, nonLocalInjectors)); + } STAT_UPDATE(entityPacketsInKbps, octreeServerCount ? totalEntityKbps / octreeServerCount : -1); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 0f563a6935..3134b223d6 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -87,6 +87,7 @@ private: \ * @property {number} audioPacketLoss - Read-only. * @property {string} audioCodec - Read-only. * @property {string} audioNoiseGate - Read-only. + * @property {Vec2} audioInjectors - Read-only. * @property {number} entityPacketsInKbps - Read-only. * * @property {number} downloads - Read-only. @@ -243,6 +244,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, audioPacketLoss, 0) STATS_PROPERTY(QString, audioCodec, QString()) STATS_PROPERTY(QString, audioNoiseGate, QString()) + STATS_PROPERTY(QVector2D, audioInjectors, QVector2D()); STATS_PROPERTY(int, entityPacketsInKbps, 0) STATS_PROPERTY(int, downloads, 0) @@ -692,6 +694,13 @@ signals: */ void audioNoiseGateChanged(); + /**jsdoc + * Triggered when the value of the audioInjectors property changes. + * @function Stats.audioInjectorsChanged + * @returns {Signal} + */ + void audioInjectorsChanged(); + /**jsdoc * Triggered when the value of the entityPacketsInKbps property changes. * @function Stats.entityPacketsInKbpsChanged diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b2e6167ffa..afe57647f3 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1354,26 +1354,28 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { // the lock guarantees that injectorBuffer, if found, is invariant - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + auto injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { + auto options = injector->getOptions(); + static const int HRTF_DATASET_INDEX = 1; - int numChannels = injector->isAmbisonic() ? AudioConstants::AMBISONIC : (injector->isStereo() ? AudioConstants::STEREO : AudioConstants::MONO); + int numChannels = options.ambisonic ? AudioConstants::AMBISONIC : (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO); size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - float gain = injector->getVolume(); + float gain = options.volume; - if (injector->isAmbisonic()) { + if (options.ambisonic) { - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); } @@ -1382,7 +1384,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // Calculate the soundfield orientation relative to the listener. // Injector orientation can be used to align a recording to our world coordinates. // - glm::quat relativeOrientation = injector->getOrientation() * glm::inverse(_orientationGetter()); + glm::quat relativeOrientation = options.orientation * glm::inverse(_orientationGetter()); // convert from Y-up (OpenGL) to Z-up (Ambisonic) coordinate system float qw = relativeOrientation.w; @@ -1394,12 +1396,12 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - } else if (injector->isStereo()) { + } else if (options.stereo) { - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); } @@ -1412,10 +1414,10 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { // injector is mono - if (injector->isPositionSet()) { + if (options.positionSet) { // distance attenuation - glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); + glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); gain = gainForSource(distance, gain); @@ -1437,21 +1439,21 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { - qCDebug(audioclient) << "injector has no more data, marking finished for removal"; + //qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; + //qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } for (const AudioInjectorPointer& injector : injectorsToRemove) { - qCDebug(audioclient) << "removing injector"; + //qCDebug(audioclient) << "removing injector"; _activeLocalAudioInjectors.removeOne(injector); } @@ -1562,15 +1564,13 @@ bool AudioClient::setIsStereoInput(bool isStereoInput) { } bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { - AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); + auto injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { // local injectors are on the AudioInjectorsThread, so we must guard access Lock lock(_injectorsMutex); if (!_activeLocalAudioInjectors.contains(injector)) { - qCDebug(audioclient) << "adding new injector"; + //qCDebug(audioclient) << "adding new injector"; _activeLocalAudioInjectors.append(injector); - // move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop()) - injectorBuffer->setParent(nullptr); // update the flag _localInjectorsAvailable.exchange(true, std::memory_order_release); @@ -1586,6 +1586,11 @@ bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { } } +int AudioClient::getNumLocalInjectors() { + Lock lock(_injectorsMutex); + return _activeLocalAudioInjectors.size(); +} + void AudioClient::outputFormatChanged() { _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 87e0f68e72..2cfe83d445 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -181,6 +181,8 @@ public: bool isHeadsetPluggedIn() { return _isHeadsetPluggedIn; } #endif + int getNumLocalInjectors(); + public slots: void start(); void stop(); diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 1581990e0c..4911917bf0 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -24,9 +24,10 @@ #include "AudioRingBuffer.h" #include "AudioLogging.h" #include "SoundCache.h" -#include "AudioSRC.h" #include "AudioHelpers.h" +int metaType = qRegisterMetaType("AudioInjectorPointer"); + AbstractAudioInterface* AudioInjector::_localAudioInterface{ nullptr }; AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { @@ -51,26 +52,30 @@ AudioInjector::AudioInjector(AudioDataPointer audioData, const AudioInjectorOpti { } -AudioInjector::~AudioInjector() { - deleteLocalBuffer(); -} +AudioInjector::~AudioInjector() {} bool AudioInjector::stateHas(AudioInjectorState state) const { - return (_state & state) == state; + return resultWithReadLock([&] { + return (_state & state) == state; + }); } void AudioInjector::setOptions(const AudioInjectorOptions& options) { // since options.stereo is computed from the audio stream, // we need to copy it from existing options just in case. - bool currentlyStereo = _options.stereo; - bool currentlyAmbisonic = _options.ambisonic; - _options = options; - _options.stereo = currentlyStereo; - _options.ambisonic = currentlyAmbisonic; + withWriteLock([&] { + bool currentlyStereo = _options.stereo; + bool currentlyAmbisonic = _options.ambisonic; + _options = options; + _options.stereo = currentlyStereo; + _options.ambisonic = currentlyAmbisonic; + }); } void AudioInjector::finishNetworkInjection() { - _state |= AudioInjectorState::NetworkInjectionFinished; + withWriteLock([&] { + _state |= AudioInjectorState::NetworkInjectionFinished; + }); // if we are already finished with local // injection, then we are finished @@ -80,35 +85,31 @@ void AudioInjector::finishNetworkInjection() { } void AudioInjector::finishLocalInjection() { - _state |= AudioInjectorState::LocalInjectionFinished; - if(_options.localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "finishLocalInjection"); + return; + } + + bool localOnly = false; + withWriteLock([&] { + _state |= AudioInjectorState::LocalInjectionFinished; + localOnly = _options.localOnly; + }); + + if(localOnly || stateHas(AudioInjectorState::NetworkInjectionFinished)) { finish(); } } void AudioInjector::finish() { - _state |= AudioInjectorState::Finished; - + withWriteLock([&] { + _state |= AudioInjectorState::Finished; + }); emit finished(); - - deleteLocalBuffer(); + _localBuffer = nullptr; } void AudioInjector::restart() { - // grab the AudioInjectorManager - auto injectorManager = DependencyManager::get(); - - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "restart"); - - if (!_options.localOnly) { - // notify the AudioInjectorManager to wake up in case it's waiting for new injectors - injectorManager->notifyInjectorReadyCondition(); - } - - return; - } - // reset the current send offset to zero _currentSendOffset = 0; @@ -121,19 +122,23 @@ void AudioInjector::restart() { // check our state to decide if we need extra handling for the restart request if (stateHas(AudioInjectorState::Finished)) { - if (!inject(&AudioInjectorManager::restartFinishedInjector)) { + if (!inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::restart failed to thread injector"; } } } bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)) { - _state = AudioInjectorState::NotFinished; + AudioInjectorOptions options; + withWriteLock([&] { + _state = AudioInjectorState::NotFinished; + options = _options; + }); int byteOffset = 0; - if (_options.secondOffset > 0.0f) { - int numChannels = _options.ambisonic ? 4 : (_options.stereo ? 2 : 1); - byteOffset = (int)(AudioConstants::SAMPLE_RATE * _options.secondOffset * numChannels); + if (options.secondOffset > 0.0f) { + int numChannels = options.ambisonic ? 4 : (options.stereo ? 2 : 1); + byteOffset = (int)(AudioConstants::SAMPLE_RATE * options.secondOffset * numChannels); byteOffset *= AudioConstants::SAMPLE_SIZE; } _currentSendOffset = byteOffset; @@ -143,7 +148,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInj } bool success = true; - if (!_options.localOnly) { + if (!options.localOnly) { auto injectorManager = DependencyManager::get(); if (!(*injectorManager.*injection)(sharedFromThis())) { success = false; @@ -158,7 +163,8 @@ bool AudioInjector::injectLocally() { if (_localAudioInterface) { if (_audioData->getNumBytes() > 0) { - _localBuffer = new AudioInjectorLocalBuffer(_audioData); + _localBuffer = QSharedPointer(new AudioInjectorLocalBuffer(_audioData), &AudioInjectorLocalBuffer::deleteLater); + _localBuffer->moveToThread(thread()); _localBuffer->open(QIODevice::ReadOnly); _localBuffer->setShouldLoop(_options.loop); @@ -181,14 +187,6 @@ bool AudioInjector::injectLocally() { return success; } -void AudioInjector::deleteLocalBuffer() { - if (_localBuffer) { - _localBuffer->stop(); - _localBuffer->deleteLater(); - _localBuffer = nullptr; - } -} - const uchar MAX_INJECTOR_VOLUME = packFloatGainToByte(1.0f); static const int64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = -1; static const int64_t NEXT_FRAME_DELTA_IMMEDIATELY = 0; @@ -220,6 +218,10 @@ int64_t AudioInjector::injectNextFrame() { static int volumeOptionOffset = -1; static int audioDataOffset = -1; + AudioInjectorOptions options = resultWithReadLock([&] { + return _options; + }); + if (!_currentPacket) { if (_currentSendOffset < 0 || _currentSendOffset >= (int)_audioData->getNumBytes()) { @@ -253,7 +255,7 @@ int64_t AudioInjector::injectNextFrame() { audioPacketStream << QUuid::createUuid(); // pack the stereo/mono type of the stream - audioPacketStream << _options.stereo; + audioPacketStream << options.stereo; // pack the flag for loopback, if requested loopbackOptionOffset = _currentPacket->pos(); @@ -262,15 +264,16 @@ int64_t AudioInjector::injectNextFrame() { // pack the position for injected audio positionOptionOffset = _currentPacket->pos(); - audioPacketStream.writeRawData(reinterpret_cast(&_options.position), - sizeof(_options.position)); + audioPacketStream.writeRawData(reinterpret_cast(&options.position), + sizeof(options.position)); // pack our orientation for injected audio - audioPacketStream.writeRawData(reinterpret_cast(&_options.orientation), - sizeof(_options.orientation)); + audioPacketStream.writeRawData(reinterpret_cast(&options.orientation), + sizeof(options.orientation)); + + audioPacketStream.writeRawData(reinterpret_cast(&options.position), + sizeof(options.position)); - audioPacketStream.writeRawData(reinterpret_cast(&_options.position), - sizeof(_options.position)); glm::vec3 boxCorner = glm::vec3(0); audioPacketStream.writeRawData(reinterpret_cast(&boxCorner), sizeof(glm::vec3)); @@ -283,7 +286,7 @@ int64_t AudioInjector::injectNextFrame() { volumeOptionOffset = _currentPacket->pos(); quint8 volume = MAX_INJECTOR_VOLUME; audioPacketStream << volume; - audioPacketStream << _options.ignorePenumbra; + audioPacketStream << options.ignorePenumbra; audioDataOffset = _currentPacket->pos(); @@ -313,10 +316,10 @@ int64_t AudioInjector::injectNextFrame() { _currentPacket->writePrimitive((uchar)(_localAudioInterface && _localAudioInterface->shouldLoopbackInjectors())); _currentPacket->seek(positionOptionOffset); - _currentPacket->writePrimitive(_options.position); - _currentPacket->writePrimitive(_options.orientation); + _currentPacket->writePrimitive(options.position); + _currentPacket->writePrimitive(options.orientation); - quint8 volume = packFloatGainToByte(_options.volume); + quint8 volume = packFloatGainToByte(options.volume); _currentPacket->seek(volumeOptionOffset); _currentPacket->writePrimitive(volume); @@ -326,8 +329,8 @@ int64_t AudioInjector::injectNextFrame() { // Might be a reasonable place to do the encode step here. QByteArray decodedAudio; - int totalBytesLeftToCopy = (_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - if (!_options.loop) { + int totalBytesLeftToCopy = (options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + if (!options.loop) { // If we aren't looping, let's make sure we don't read past the end int bytesLeftToRead = _audioData->getNumBytes() - _currentSendOffset; totalBytesLeftToCopy = std::min(totalBytesLeftToCopy, bytesLeftToRead); @@ -342,14 +345,16 @@ int64_t AudioInjector::injectNextFrame() { auto samplesOut = reinterpret_cast(decodedAudio.data()); // Copy and Measure the loudness of this frame - _loudness = 0.0f; - for (int i = 0; i < samplesLeftToCopy; ++i) { - auto index = (currentSample + i) % _audioData->getNumSamples(); - auto sample = samples[index]; - samplesOut[i] = sample; - _loudness += abs(sample) / (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); - } - _loudness /= (float)samplesLeftToCopy; + withWriteLock([&] { + _loudness = 0.0f; + for (int i = 0; i < samplesLeftToCopy; ++i) { + auto index = (currentSample + i) % _audioData->getNumSamples(); + auto sample = samples[index]; + samplesOut[i] = sample; + _loudness += abs(sample) / (AudioConstants::MAX_SAMPLE_VALUE / 2.0f); + } + _loudness /= (float)samplesLeftToCopy; + }); _currentSendOffset = (_currentSendOffset + totalBytesLeftToCopy) % _audioData->getNumBytes(); @@ -371,7 +376,7 @@ int64_t AudioInjector::injectNextFrame() { _outgoingSequenceNumber++; } - if (_currentSendOffset == 0 && !_options.loop) { + if (_currentSendOffset == 0 && !options.loop) { finishNetworkInjection(); return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } @@ -391,134 +396,10 @@ int64_t AudioInjector::injectNextFrame() { // If we are falling behind by more frames than our threshold, let's skip the frames ahead qCDebug(audio) << this << "injectNextFrame() skipping ahead, fell behind by " << (currentFrameBasedOnElapsedTime - _nextFrame) << " frames"; _nextFrame = currentFrameBasedOnElapsedTime; - _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (_options.stereo ? 2 : 1) % _audioData->getNumBytes(); + _currentSendOffset = _nextFrame * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * (options.stereo ? 2 : 1) % _audioData->getNumBytes(); } int64_t playNextFrameAt = ++_nextFrame * AudioConstants::NETWORK_FRAME_USECS; return std::max(INT64_C(0), playNextFrameAt - currentTime); -} - -void AudioInjector::stop() { - // trigger a call on the injector's thread to change state to finished - QMetaObject::invokeMethod(this, "finish"); -} - -void AudioInjector::triggerDeleteAfterFinish() { - // make sure this fires on the AudioInjector thread - if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "triggerDeleteAfterFinish", Qt::QueuedConnection); - return; - } - - if (stateHas(AudioInjectorState::Finished)) { - stop(); - } else { - _state |= AudioInjectorState::PendingDelete; - } -} - -AudioInjectorPointer AudioInjector::playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options) { - AudioInjectorPointer injector = playSound(sound, options); - - if (injector) { - injector->_state |= AudioInjectorState::PendingDelete; - } - - return injector; -} - - -AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const AudioInjectorOptions& options) { - if (!sound || !sound->isReady()) { - return AudioInjectorPointer(); - } - - if (options.pitch == 1.0f) { - - AudioInjectorPointer injector = AudioInjectorPointer::create(sound, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread injector"; - } - return injector; - - } else { - using AudioConstants::AudioSample; - using AudioConstants::SAMPLE_RATE; - const int standardRate = SAMPLE_RATE; - // limit pitch to 4 octaves - const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); - const int resampledRate = glm::round(SAMPLE_RATE / pitch); - - auto audioData = sound->getAudioData(); - auto numChannels = audioData->getNumChannels(); - auto numFrames = audioData->getNumFrames(); - - AudioSRC resampler(standardRate, resampledRate, numChannels); - - // create a resampled buffer that is guaranteed to be large enough - const int maxOutputFrames = resampler.getMaxOutput(numFrames); - const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); - QByteArray resampledBuffer(maxOutputSize, '\0'); - auto bufferPtr = reinterpret_cast(resampledBuffer.data()); - - resampler.render(audioData->data(), bufferPtr, numFrames); - - int numSamples = maxOutputFrames * numChannels; - auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); - - AudioInjectorPointer injector = AudioInjectorPointer::create(newAudioData, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; - } - return injector; - } -} - -AudioInjectorPointer AudioInjector::playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options) { - AudioInjectorPointer injector = playSound(audioData, options); - - if (injector) { - injector->_state |= AudioInjectorState::PendingDelete; - } - - return injector; -} - -AudioInjectorPointer AudioInjector::playSound(AudioDataPointer audioData, const AudioInjectorOptions& options) { - if (options.pitch == 1.0f) { - AudioInjectorPointer injector = AudioInjectorPointer::create(audioData, options); - - if (!injector->inject(&AudioInjectorManager::threadInjector)) { - qWarning() << "AudioInjector::playSound failed to thread pitch-shifted injector"; - } - return injector; - } else { - using AudioConstants::AudioSample; - using AudioConstants::SAMPLE_RATE; - const int standardRate = SAMPLE_RATE; - // limit pitch to 4 octaves - const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); - const int resampledRate = glm::round(SAMPLE_RATE / pitch); - - auto numChannels = audioData->getNumChannels(); - auto numFrames = audioData->getNumFrames(); - - AudioSRC resampler(standardRate, resampledRate, numChannels); - - // create a resampled buffer that is guaranteed to be large enough - const int maxOutputFrames = resampler.getMaxOutput(numFrames); - const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); - QByteArray resampledBuffer(maxOutputSize, '\0'); - auto bufferPtr = reinterpret_cast(resampledBuffer.data()); - - resampler.render(audioData->data(), bufferPtr, numFrames); - - int numSamples = maxOutputFrames * numChannels; - auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); - - return AudioInjector::playSound(newAudioData, options); - } } \ No newline at end of file diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 3c21d2eccf..1d5cf50033 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -19,6 +19,8 @@ #include #include +#include + #include #include @@ -49,7 +51,7 @@ AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs) // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object // until it dies. -class AudioInjector : public QObject, public QEnableSharedFromThis { +class AudioInjector : public QObject, public QEnableSharedFromThis, public ReadWriteLockable { Q_OBJECT public: AudioInjector(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions); @@ -61,38 +63,30 @@ public: int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } - AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } + QSharedPointer getLocalBuffer() const { return _localBuffer; } AudioHRTF& getLocalHRTF() { return _localHRTF; } AudioFOA& getLocalFOA() { return _localFOA; } - bool isLocalOnly() const { return _options.localOnly; } - float getVolume() const { return _options.volume; } - bool isPositionSet() const { return _options.positionSet; } - glm::vec3 getPosition() const { return _options.position; } - glm::quat getOrientation() const { return _options.orientation; } - bool isStereo() const { return _options.stereo; } - bool isAmbisonic() const { return _options.ambisonic; } + float getLoudness() const { return resultWithReadLock([&] { return _loudness; }); } + bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } + + bool isLocalOnly() const { return resultWithReadLock([&] { return _options.localOnly; }); } + float getVolume() const { return resultWithReadLock([&] { return _options.volume; }); } + bool isPositionSet() const { return resultWithReadLock([&] { return _options.positionSet; }); } + glm::vec3 getPosition() const { return resultWithReadLock([&] { return _options.position; }); } + glm::quat getOrientation() const { return resultWithReadLock([&] { return _options.orientation; }); } + bool isStereo() const { return resultWithReadLock([&] { return _options.stereo; }); } + bool isAmbisonic() const { return resultWithReadLock([&] { return _options.ambisonic; }); } + + const AudioInjectorOptions& getOptions() const { return resultWithReadLock([&] { return _options; }); } + void setOptions(const AudioInjectorOptions& options); bool stateHas(AudioInjectorState state) const ; static void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - static AudioInjectorPointer playSoundAndDelete(SharedSoundPointer sound, const AudioInjectorOptions& options); - static AudioInjectorPointer playSound(SharedSoundPointer sound, const AudioInjectorOptions& options); - static AudioInjectorPointer playSoundAndDelete(AudioDataPointer audioData, const AudioInjectorOptions& options); - static AudioInjectorPointer playSound(AudioDataPointer audioData, const AudioInjectorOptions& options); - -public slots: void restart(); - - void stop(); - void triggerDeleteAfterFinish(); - - const AudioInjectorOptions& getOptions() const { return _options; } - void setOptions(const AudioInjectorOptions& options); - - float getLoudness() const { return _loudness; } - bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } void finish(); + void finishLocalInjection(); void finishNetworkInjection(); @@ -104,7 +98,6 @@ private: int64_t injectNextFrame(); bool inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)); bool injectLocally(); - void deleteLocalBuffer(); static AbstractAudioInterface* _localAudioInterface; @@ -116,7 +109,7 @@ private: float _loudness { 0.0f }; int _currentSendOffset { 0 }; std::unique_ptr _currentPacket { nullptr }; - AudioInjectorLocalBuffer* _localBuffer { nullptr }; + QSharedPointer _localBuffer { nullptr }; int64_t _nextFrame { 0 }; std::unique_ptr _frameTimer { nullptr }; @@ -128,4 +121,6 @@ private: friend class AudioInjectorManager; }; +Q_DECLARE_METATYPE(AudioInjectorPointer) + #endif // hifi_AudioInjector_h diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.cpp b/libraries/audio/src/AudioInjectorLocalBuffer.cpp index 015d87e03b..680513abf5 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.cpp +++ b/libraries/audio/src/AudioInjectorLocalBuffer.cpp @@ -16,6 +16,10 @@ AudioInjectorLocalBuffer::AudioInjectorLocalBuffer(AudioDataPointer audioData) : { } +AudioInjectorLocalBuffer::~AudioInjectorLocalBuffer() { + stop(); +} + void AudioInjectorLocalBuffer::stop() { _isStopped = true; @@ -30,9 +34,8 @@ bool AudioInjectorLocalBuffer::seek(qint64 pos) { } } - qint64 AudioInjectorLocalBuffer::readData(char* data, qint64 maxSize) { - if (!_isStopped) { + if (!_isStopped && _audioData) { // first copy to the end of the raw audio int bytesToEnd = (int)_audioData->getNumBytes() - _currentOffset; diff --git a/libraries/audio/src/AudioInjectorLocalBuffer.h b/libraries/audio/src/AudioInjectorLocalBuffer.h index e0f8847883..2f73e5b313 100644 --- a/libraries/audio/src/AudioInjectorLocalBuffer.h +++ b/libraries/audio/src/AudioInjectorLocalBuffer.h @@ -22,6 +22,7 @@ class AudioInjectorLocalBuffer : public QIODevice { Q_OBJECT public: AudioInjectorLocalBuffer(AudioDataPointer audioData); + ~AudioInjectorLocalBuffer(); void stop(); diff --git a/libraries/audio/src/AudioInjectorManager.cpp b/libraries/audio/src/AudioInjectorManager.cpp index f30d3093ec..e5ffc77798 100644 --- a/libraries/audio/src/AudioInjectorManager.cpp +++ b/libraries/audio/src/AudioInjectorManager.cpp @@ -14,11 +14,14 @@ #include #include +#include #include "AudioConstants.h" #include "AudioInjector.h" #include "AudioLogging.h" +#include "AudioSRC.h" + AudioInjectorManager::~AudioInjectorManager() { _shouldStop = true; @@ -30,7 +33,7 @@ AudioInjectorManager::~AudioInjectorManager() { auto& timePointerPair = _injectors.top(); // ask it to stop and be deleted - timePointerPair.second->stop(); + timePointerPair.second->finish(); _injectors.pop(); } @@ -46,6 +49,8 @@ AudioInjectorManager::~AudioInjectorManager() { _thread->quit(); _thread->wait(); } + + moveToThread(qApp->thread()); } void AudioInjectorManager::createThread() { @@ -55,6 +60,8 @@ void AudioInjectorManager::createThread() { // when the thread is started, have it call our run to handle injection of audio connect(_thread, &QThread::started, this, &AudioInjectorManager::run, Qt::DirectConnection); + moveToThread(_thread); + // start the thread _thread->start(); } @@ -141,36 +148,7 @@ bool AudioInjectorManager::wouldExceedLimits() { // Should be called inside of a bool AudioInjectorManager::threadInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { - qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; - return false; - } - - // guard the injectors vector with a mutex - Lock lock(_injectorsMutex); - - if (wouldExceedLimits()) { - return false; - } else { - if (!_thread) { - createThread(); - } - - // move the injector to the QThread - injector->moveToThread(_thread); - - // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), injector); - - // notify our wait condition so we can inject two frames for this injector immediately - _injectorReady.notify_one(); - - return true; - } -} - -bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& injector) { - if (_shouldStop) { - qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; } @@ -188,3 +166,192 @@ bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& i } return true; } + +AudioInjectorPointer AudioInjectorManager::playSound(const SharedSoundPointer& sound, const AudioInjectorOptions& options, bool setPendingDelete) { + if (_shouldStop) { + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + return nullptr; + } + + AudioInjectorPointer injector = nullptr; + if (sound && sound->isReady()) { + if (options.pitch == 1.0f) { + injector = QSharedPointer(new AudioInjector(sound, options), &AudioInjector::deleteLater); + } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit pitch to 4 octaves + const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = glm::round(SAMPLE_RATE / pitch); + + auto audioData = sound->getAudioData(); + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); + + resampler.render(audioData->data(), bufferPtr, numFrames); + + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + injector = QSharedPointer(new AudioInjector(newAudioData, options), &AudioInjector::deleteLater); + } + } + + if (!injector) { + return nullptr; + } + + if (setPendingDelete) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + injector->moveToThread(_thread); + injector->inject(&AudioInjectorManager::threadInjector); + + return injector; +} + +AudioInjectorPointer AudioInjectorManager::playSound(const AudioDataPointer& audioData, const AudioInjectorOptions& options, bool setPendingDelete) { + if (_shouldStop) { + qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; + return nullptr; + } + + AudioInjectorPointer injector = nullptr; + if (options.pitch == 1.0f) { + injector = QSharedPointer(new AudioInjector(audioData, options), &AudioInjector::deleteLater); + } else { + using AudioConstants::AudioSample; + using AudioConstants::SAMPLE_RATE; + const int standardRate = SAMPLE_RATE; + // limit pitch to 4 octaves + const float pitch = glm::clamp(options.pitch, 1 / 16.0f, 16.0f); + const int resampledRate = glm::round(SAMPLE_RATE / pitch); + + auto numChannels = audioData->getNumChannels(); + auto numFrames = audioData->getNumFrames(); + + AudioSRC resampler(standardRate, resampledRate, numChannels); + + // create a resampled buffer that is guaranteed to be large enough + const int maxOutputFrames = resampler.getMaxOutput(numFrames); + const int maxOutputSize = maxOutputFrames * numChannels * sizeof(AudioSample); + QByteArray resampledBuffer(maxOutputSize, '\0'); + auto bufferPtr = reinterpret_cast(resampledBuffer.data()); + + resampler.render(audioData->data(), bufferPtr, numFrames); + + int numSamples = maxOutputFrames * numChannels; + auto newAudioData = AudioData::make(numSamples, numChannels, bufferPtr); + + injector = QSharedPointer(new AudioInjector(newAudioData, options), &AudioInjector::deleteLater); + } + + if (!injector) { + return nullptr; + } + + if (setPendingDelete) { + injector->_state |= AudioInjectorState::PendingDelete; + } + + injector->moveToThread(_thread); + injector->inject(&AudioInjectorManager::threadInjector); + + return injector; +} + +void AudioInjectorManager::setOptionsAndRestart(const AudioInjectorPointer& injector, const AudioInjectorOptions& options) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "setOptionsAndRestart", Q_ARG(const AudioInjectorPointer&, injector), Q_ARG(const AudioInjectorOptions&, options)); + _injectorReady.notify_one(); + return; + } + + injector->setOptions(options); + injector->restart(); +} + +void AudioInjectorManager::restart(const AudioInjectorPointer& injector) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "restart", Q_ARG(const AudioInjectorPointer&, injector)); + _injectorReady.notify_one(); + return; + } + + injector->restart(); +} + +void AudioInjectorManager::setOptions(const AudioInjectorPointer& injector, const AudioInjectorOptions& options) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "setOptions", Q_ARG(const AudioInjectorPointer&, injector), Q_ARG(const AudioInjectorOptions&, options)); + _injectorReady.notify_one(); + return; + } + + injector->setOptions(options); +} + +AudioInjectorOptions AudioInjectorManager::getOptions(const AudioInjectorPointer& injector) { + if (!injector) { + return AudioInjectorOptions(); + } + + return injector->getOptions(); +} + +float AudioInjectorManager::getLoudness(const AudioInjectorPointer& injector) { + if (!injector) { + return 0.0f; + } + + return injector->getLoudness(); +} + +bool AudioInjectorManager::isPlaying(const AudioInjectorPointer& injector) { + if (!injector) { + return false; + } + + return injector->isPlaying(); +} + +void AudioInjectorManager::stop(const AudioInjectorPointer& injector) { + if (!injector) { + return; + } + + if (QThread::currentThread() != _thread) { + QMetaObject::invokeMethod(this, "stop", Q_ARG(const AudioInjectorPointer&, injector)); + _injectorReady.notify_one(); + return; + } + + injector->finish(); +} + +size_t AudioInjectorManager::getNumInjectors() { + Lock lock(_injectorsMutex); + return _injectors.size(); +} \ No newline at end of file diff --git a/libraries/audio/src/AudioInjectorManager.h b/libraries/audio/src/AudioInjectorManager.h index 9aca3014e3..cb243f23de 100644 --- a/libraries/audio/src/AudioInjectorManager.h +++ b/libraries/audio/src/AudioInjectorManager.h @@ -30,8 +30,27 @@ class AudioInjectorManager : public QObject, public Dependency { SINGLETON_DEPENDENCY public: ~AudioInjectorManager(); + + AudioInjectorPointer playSound(const SharedSoundPointer& sound, const AudioInjectorOptions& options, bool setPendingDelete = false); + AudioInjectorPointer playSound(const AudioDataPointer& audioData, const AudioInjectorOptions& options, bool setPendingDelete = false); + + size_t getNumInjectors(); + +public slots: + void setOptionsAndRestart(const AudioInjectorPointer& injector, const AudioInjectorOptions& options); + void restart(const AudioInjectorPointer& injector); + + void setOptions(const AudioInjectorPointer& injector, const AudioInjectorOptions& options); + AudioInjectorOptions getOptions(const AudioInjectorPointer& injector); + + float getLoudness(const AudioInjectorPointer& injector); + bool isPlaying(const AudioInjectorPointer& injector); + + void stop(const AudioInjectorPointer& injector); + private slots: void run(); + private: using TimeInjectorPointerPair = std::pair; @@ -49,11 +68,10 @@ private: using Lock = std::unique_lock; bool threadInjector(const AudioInjectorPointer& injector); - bool restartFinishedInjector(const AudioInjectorPointer& injector); void notifyInjectorReadyCondition() { _injectorReady.notify_one(); } bool wouldExceedLimits(); - AudioInjectorManager() {}; + AudioInjectorManager() { createThread(); } AudioInjectorManager(const AudioInjectorManager&) = delete; AudioInjectorManager& operator=(const AudioInjectorManager&) = delete; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 143c7fa377..c235460404 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1105,7 +1105,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const EntityItemPointer& entit options.volume = volume; options.pitch = 1.0f / stretchFactor; - AudioInjector::playSoundAndDelete(collisionSound, options); + DependencyManager::get()->playSound(collisionSound, options, true); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index a257951ba8..a511d73210 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -16,7 +16,7 @@ #include #include -#include +#include #include // for RayToEntityIntersectionResult #include #include diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 65d71e46e6..395571c51f 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -63,7 +63,7 @@ ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound optionsCopy.ambisonic = sound->isAmbisonic(); optionsCopy.localOnly = optionsCopy.localOnly || sound->isAmbisonic(); // force localOnly when Ambisonic - auto injector = AudioInjector::playSound(sound, optionsCopy); + auto injector = DependencyManager::get()->playSound(sound, optionsCopy); if (!injector) { return nullptr; } diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index 8b51377bff..822aa0a9c1 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -20,8 +20,11 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c } // when the script goes down we want to cleanup the injector - QObject::connect(engine, &QScriptEngine::destroyed, in, &ScriptAudioInjector::stopInjectorImmediately, - Qt::DirectConnection); + QObject::connect(engine, &QScriptEngine::destroyed, DependencyManager::get().data(), [&] { + qCDebug(scriptengine) << "Script was shutdown, stopping an injector"; + // FIXME: this doesn't work and leaves the injectors lying around + //DependencyManager::get()->stop(in->_injector); + }); return engine->newQObject(in, QScriptEngine::ScriptOwnership); } @@ -37,13 +40,5 @@ ScriptAudioInjector::ScriptAudioInjector(const AudioInjectorPointer& injector) : } ScriptAudioInjector::~ScriptAudioInjector() { - if (!_injector.isNull()) { - // we've been asked to delete after finishing, trigger a queued deleteLater here - _injector->triggerDeleteAfterFinish(); - } -} - -void ScriptAudioInjector::stopInjectorImmediately() { - qCDebug(scriptengine) << "ScriptAudioInjector::stopInjectorImmediately called to stop audio injector immediately."; - _injector->stop(); -} + DependencyManager::get()->stop(_injector); +} \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptAudioInjector.h b/libraries/script-engine/src/ScriptAudioInjector.h index c7fb2f8a9a..d77291b92c 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.h +++ b/libraries/script-engine/src/ScriptAudioInjector.h @@ -14,7 +14,7 @@ #include -#include +#include /**jsdoc * Plays — "injects" — the content of an audio file. Used in the {@link Audio} API. @@ -48,7 +48,7 @@ public slots: * Stop current playback, if any, and start playing from the beginning. * @function AudioInjector.restart */ - void restart() { _injector->restart(); } + void restart() { DependencyManager::get()->restart(_injector); } /**jsdoc * Stop audio playback. @@ -68,28 +68,28 @@ public slots: * injector.stop(); * }, 2000); */ - void stop() { _injector->stop(); } + void stop() { DependencyManager::get()->stop(_injector); } /**jsdoc * Get the current configuration of the audio injector. * @function AudioInjector.getOptions * @returns {AudioInjector.AudioInjectorOptions} Configuration of how the injector plays the audio. */ - const AudioInjectorOptions& getOptions() const { return _injector->getOptions(); } + AudioInjectorOptions getOptions() const { return DependencyManager::get()->getOptions(_injector); } /**jsdoc * Configure how the injector plays the audio. * @function AudioInjector.setOptions * @param {AudioInjector.AudioInjectorOptions} options - Configuration of how the injector plays the audio. */ - void setOptions(const AudioInjectorOptions& options) { _injector->setOptions(options); } + void setOptions(const AudioInjectorOptions& options) { DependencyManager::get()->setOptions(_injector, options); } /**jsdoc * Get the loudness of the most recent frame of audio played. * @function AudioInjector.getLoudness * @returns {number} The loudness of the most recent frame of audio played, range 0.01.0. */ - float getLoudness() const { return _injector->getLoudness(); } + float getLoudness() const { return DependencyManager::get()->getLoudness(_injector); } /**jsdoc * Get whether or not the audio is currently playing. @@ -110,7 +110,7 @@ public slots: * print("Sound is playing: " + injector.isPlaying()); * }, 2000); */ - bool isPlaying() const { return _injector->isPlaying(); } + bool isPlaying() const { return DependencyManager::get()->isPlaying(_injector); } signals: @@ -134,13 +134,6 @@ signals: */ void finished(); -protected slots: - - /**jsdoc - * Stop audio playback. (Synonym of {@link AudioInjector.stop|stop}.) - * @function AudioInjector.stopInjectorImmediately - */ - void stopInjectorImmediately(); private: AudioInjectorPointer _injector; diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 7a1c37af33..963e0d87c1 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -23,7 +23,7 @@ #include "ToolbarScriptingInterface.h" #include "Logging.h" -#include +#include #include "SettingHandle.h" @@ -212,7 +212,7 @@ void TabletScriptingInterface::playSound(TabletAudioEvents aEvent) { options.localOnly = true; options.positionSet = false; // system sound - AudioInjectorPointer injector = AudioInjector::playSoundAndDelete(sound, options); + DependencyManager::get()->playSound(sound, options, true); } } diff --git a/libraries/ui/src/ui/types/SoundEffect.cpp b/libraries/ui/src/ui/types/SoundEffect.cpp index dc2328b33e..38114ecef1 100644 --- a/libraries/ui/src/ui/types/SoundEffect.cpp +++ b/libraries/ui/src/ui/types/SoundEffect.cpp @@ -2,12 +2,10 @@ #include "SoundEffect.h" #include -#include SoundEffect::~SoundEffect() { if (_injector) { - // stop will cause the AudioInjector to delete itself. - _injector->stop(); + DependencyManager::get()->stop(_injector); } } @@ -28,15 +26,14 @@ void SoundEffect::setVolume(float volume) { _volume = volume; } -void SoundEffect::play(QVariant position) { +void SoundEffect::play(const QVariant& position) { AudioInjectorOptions options; options.position = vec3FromVariant(position); options.localOnly = true; options.volume = _volume; if (_injector) { - _injector->setOptions(options); - _injector->restart(); + DependencyManager::get()->setOptionsAndRestart(_injector, options); } else { - _injector = AudioInjector::playSound(_sound, options); + _injector = DependencyManager::get()->playSound(_sound, options); } } diff --git a/libraries/ui/src/ui/types/SoundEffect.h b/libraries/ui/src/ui/types/SoundEffect.h index a7e29d86f9..cb8a5cd67f 100644 --- a/libraries/ui/src/ui/types/SoundEffect.h +++ b/libraries/ui/src/ui/types/SoundEffect.h @@ -13,9 +13,7 @@ #include #include - -class AudioInjector; -using AudioInjectorPointer = QSharedPointer; +#include // SoundEffect object, exposed to qml only, not interface JavaScript. // This is used to play spatial sound effects on tablets/web entities from within QML. @@ -34,7 +32,7 @@ public: float getVolume() const; void setVolume(float volume); - Q_INVOKABLE void play(QVariant position); + Q_INVOKABLE void play(const QVariant& position); protected: QUrl _url; float _volume { 1.0f }; From f013b9af2b6691a85da36f153be482e74b6574e9 Mon Sep 17 00:00:00 2001 From: Sam Gondelman Date: Wed, 13 Mar 2019 00:24:19 -0700 Subject: [PATCH 046/117] fix warnings --- libraries/audio/src/AudioInjector.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 1d5cf50033..94527cfdd0 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -78,7 +78,7 @@ public: bool isStereo() const { return resultWithReadLock([&] { return _options.stereo; }); } bool isAmbisonic() const { return resultWithReadLock([&] { return _options.ambisonic; }); } - const AudioInjectorOptions& getOptions() const { return resultWithReadLock([&] { return _options; }); } + AudioInjectorOptions getOptions() const { return resultWithReadLock([&] { return _options; }); } void setOptions(const AudioInjectorOptions& options); bool stateHas(AudioInjectorState state) const ; From 2dc38df779b62b58fc16426f8e86c522d314fe61 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Wed, 13 Mar 2019 10:17:39 -0700 Subject: [PATCH 047/117] Case 20393 - Display item counts in categories dropdown in Marketplace --- .../hifi/commerce/marketplace/Marketplace.qml | 68 ++++++++++++------- .../src/avatar/MarketplaceItemUploader.cpp | 2 +- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 07ded49956..601a04080a 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -87,22 +87,11 @@ Rectangle { console.log("Failed to get Marketplace Categories", result.data.message); } else { categoriesModel.clear(); - categoriesModel.append({ - id: -1, - name: "Everything" - }); - categoriesModel.append({ - id: -1, - name: "Stand-alone Optimized" - }); - categoriesModel.append({ - id: -1, - name: "Stand-alone Compatible" - }); - result.data.items.forEach(function(category) { + result.data.categories.forEach(function(category) { categoriesModel.append({ id: category.id, - name: category.name + name: category.name, + count: category.count }); }); } @@ -396,12 +385,12 @@ Rectangle { Rectangle { anchors { - left: parent.left; - bottom: parent.bottom; - top: parent.top; - topMargin: 100; + left: parent.left + bottom: parent.bottom + top: parent.top + topMargin: 100 } - width: parent.width/3 + width: parent.width/2 color: hifi.colors.white @@ -432,20 +421,49 @@ Rectangle { color: hifi.colors.white visible: true - RalewayRegular { + RalewaySemiBold { id: categoriesItemText anchors.leftMargin: 15 - anchors.fill:parent + anchors.top:parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + elide: Text.ElideRight text: model.name color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter size: 14 } + Rectangle { + id: categoryItemCount + anchors { + top: parent.top + bottom: parent.bottom + topMargin: 5 + bottomMargin: 5 + leftMargin: 10 + rightMargin: 10 + left: categoriesItemText.right + } + width: childrenRect.width + color: hifi.colors.blueHighlight + radius: height/2 + + RalewaySemiBold { + anchors.top: parent.top + anchors.bottom: parent.bottom + width: paintedWidth+30 + + text: model.count + color: hifi.colors.white + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + size: 16 + } + } } - MouseArea { anchors.fill: parent z: 10 @@ -476,9 +494,9 @@ Rectangle { parent: categoriesListView.parent anchors { - top: categoriesListView.top; - bottom: categoriesListView.bottom; - left: categoriesListView.right; + top: categoriesListView.top + bottom: categoriesListView.bottom + left: categoriesListView.right } contentItem.opacity: 1 diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 53b37eba4f..28b07780b0 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -87,7 +87,7 @@ void MarketplaceItemUploader::doGetCategories() { if (error == QNetworkReply::NoError) { auto doc = QJsonDocument::fromJson(reply->readAll()); auto extractCategoryID = [&doc]() -> std::pair { - auto items = doc.object()["data"].toObject()["items"]; + auto items = doc.object()["data"].toObject()["categories"]; if (!items.isArray()) { qWarning() << "Categories parse error: data.items is not an array"; return { false, 0 }; From 300dd39abf76459eddb7b7da2f58e9bb665df433 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 13 Mar 2019 12:23:31 -0700 Subject: [PATCH 048/117] fix script engine shutdown --- interface/src/ui/Stats.cpp | 2 +- libraries/audio/src/AudioInjector.h | 4 +++- .../script-engine/src/AudioScriptingInterface.cpp | 10 ---------- libraries/script-engine/src/ScriptAudioInjector.cpp | 7 ------- libraries/script-engine/src/ScriptEngine.cpp | 7 +------ libraries/script-engine/src/ScriptEngines.cpp | 2 ++ 6 files changed, 7 insertions(+), 25 deletions(-) diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 3c943028f5..022b57c0d9 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -268,7 +268,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); { int localInjectors = audioClient->getNumLocalInjectors(); - int nonLocalInjectors = DependencyManager::get()->getNumInjectors(); + size_t nonLocalInjectors = DependencyManager::get()->getNumInjectors(); STAT_UPDATE(audioInjectors, QVector2D(localInjectors, nonLocalInjectors)); } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 94527cfdd0..555af84025 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -87,9 +87,11 @@ public: void restart(); void finish(); - void finishLocalInjection(); void finishNetworkInjection(); +public slots: + void finishLocalInjection(); + signals: void finished(); void restarting(); diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 395571c51f..a55cac292f 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -46,16 +46,6 @@ ScriptAudioInjector* AudioScriptingInterface::playSystemSound(SharedSoundPointer } ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions) { - if (QThread::currentThread() != thread()) { - ScriptAudioInjector* injector = NULL; - - BLOCKING_INVOKE_METHOD(this, "playSound", - Q_RETURN_ARG(ScriptAudioInjector*, injector), - Q_ARG(SharedSoundPointer, sound), - Q_ARG(const AudioInjectorOptions&, injectorOptions)); - return injector; - } - if (sound) { // stereo option isn't set from script, this comes from sound metadata or filename AudioInjectorOptions optionsCopy = injectorOptions; diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index 822aa0a9c1..267ac3339d 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -19,13 +19,6 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c return QScriptValue(QScriptValue::NullValue); } - // when the script goes down we want to cleanup the injector - QObject::connect(engine, &QScriptEngine::destroyed, DependencyManager::get().data(), [&] { - qCDebug(scriptengine) << "Script was shutdown, stopping an injector"; - // FIXME: this doesn't work and leaves the injectors lying around - //DependencyManager::get()->stop(in->_injector); - }); - return engine->newQObject(in, QScriptEngine::ScriptOwnership); } diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 825017b1fe..a4fd2540d4 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -260,12 +260,7 @@ bool ScriptEngine::isDebugMode() const { #endif } -ScriptEngine::~ScriptEngine() { - QSharedPointer scriptEngines(_scriptEngines); - if (scriptEngines) { - scriptEngines->removeScriptEngine(qSharedPointerCast(sharedFromThis())); - } -} +ScriptEngine::~ScriptEngine() {} void ScriptEngine::disconnectNonEssentialSignals() { disconnect(); diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 3963ad5593..25c330e3fe 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -591,6 +591,8 @@ void ScriptEngines::onScriptFinished(const QString& rawScriptURL, ScriptEnginePo } } + removeScriptEngine(engine); + if (removed && !_isReloading) { // Update settings with removed script saveScripts(); From 32d5f7135f6e40ef591214ed700ec14f29cb1776 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 14:50:50 -0700 Subject: [PATCH 049/117] Give the oven model-baker Baker an appropriate materialMappingBaseURL, but disable ParseMaterialMappingTask for now --- libraries/baking/src/ModelBaker.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index d38b965f6d..c120153ddf 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -249,12 +249,15 @@ void ModelBaker::bakeSourceCopy() { serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); - baker::Baker baker(loadedModel, serializerMapping, hifi::URL()); + baker::Baker baker(loadedModel, serializerMapping, _mappingURL); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation config->getJobConfig("BuildDracoMesh")->setEnabled(true); // Do not permit potentially lossy modification of joint data meant for runtime ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; + // The resources parsed from this job will not be used for now + // TODO: Proper full baking of all materials for a model + config->getJobConfig("ParseMaterialMapping")->setEnabled(false); // Begin hfm baking baker.run(); From 3aaa18f529e1d9dcc5bf2050da450db324c28e4b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 15:01:42 -0700 Subject: [PATCH 050/117] Might as well deduplicate indices when loading model for baking --- libraries/baking/src/ModelBaker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index c120153ddf..b6378d6503 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -247,6 +247,7 @@ void ModelBaker::bakeSourceCopy() { } hifi::VariantHash serializerMapping = _mapping; serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library + serializerMapping["deduplicateIndices"] = true; // Draco compression also deduplicates, but we might as well shave it off to save on some earlier processing (currently FBXSerializer only) hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); baker::Baker baker(loadedModel, serializerMapping, _mappingURL); From cb1f42afe53ea7fd7c28be644a9c136b65956763 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 15:16:58 -0700 Subject: [PATCH 051/117] Copy pre-parsed node from FBXSerializer for baking --- libraries/baking/src/FBXBaker.cpp | 43 ++++++++++------------------- libraries/baking/src/FBXBaker.h | 1 - libraries/baking/src/ModelBaker.cpp | 9 ++++++ 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index d2dc86c783..5e346ab8c8 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -50,35 +50,7 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { _hfmModel = hfmModel; - // Load the root node from the FBX file - importScene(); - - if (shouldStop()) { - return; - } - - // enumerate the models and textures found in the scene and start a bake for them - rewriteAndBakeSceneTextures(); - - if (shouldStop()) { - return; - } - - rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists); -} - -void FBXBaker::importScene() { - qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists(); - - QFile fbxFile(_originalModelFilePath); - if (!fbxFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); - return; - } - - qCDebug(model_baking) << "Parsing" << _modelURL; - _rootNode = FBXSerializer().parseFBX(&fbxFile); - + #ifdef HIFI_DUMP_FBX { FBXToJSON fbxToJSON; @@ -92,6 +64,19 @@ void FBXBaker::importScene() { } } #endif + + if (shouldStop()) { + return; + } + + // enumerate the models and textures found in the scene and start a bake for them + rewriteAndBakeSceneTextures(); + + if (shouldStop()) { + return; + } + + rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists); } void FBXBaker::replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index f8a023f431..3c95273f8f 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -38,7 +38,6 @@ protected: virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) override; private: - void importScene(); void rewriteAndBakeSceneModels(const QVector& meshes, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists); void rewriteAndBakeSceneTextures(); void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index b6378d6503..d906abca8f 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -250,6 +251,14 @@ void ModelBaker::bakeSourceCopy() { serializerMapping["deduplicateIndices"] = true; // Draco compression also deduplicates, but we might as well shave it off to save on some earlier processing (currently FBXSerializer only) hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); + // Temporarily support copying the pre-parsed node from FBXSerializer, for better performance in FBXBaker + // TODO: Pure HFM baking + std::shared_ptr fbxSerializer = std::dynamic_pointer_cast(serializer); + if (fbxSerializer) { + qCDebug(model_baking) << "Parsing" << _modelURL; + _rootNode = fbxSerializer->_rootNode; + } + baker::Baker baker(loadedModel, serializerMapping, _mappingURL); auto config = baker.getConfiguration(); // Enable compressed draco mesh generation From a3412bb25ee5f42fab5d582c00faeae816d68f15 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 16:11:13 -0700 Subject: [PATCH 052/117] Attempt to fix build errors --- libraries/baking/CMakeLists.txt | 2 -- libraries/baking/src/MaterialBaker.cpp | 4 +++- libraries/baking/src/baking/FSTBaker.h | 2 +- libraries/model-baker/CMakeLists.txt | 4 +++- tools/oven/src/DomainBaker.cpp | 8 ++++---- tools/oven/src/DomainBaker.h | 8 ++++---- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/libraries/baking/CMakeLists.txt b/libraries/baking/CMakeLists.txt index aeb4346f93..73618427f6 100644 --- a/libraries/baking/CMakeLists.txt +++ b/libraries/baking/CMakeLists.txt @@ -4,5 +4,3 @@ setup_hifi_library(Concurrent) link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx model-baker task) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) - -target_draco() diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 558adedf68..24d031c39e 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -11,6 +11,8 @@ #include "MaterialBaker.h" +#include + #include "QJsonObject" #include "QJsonDocument" @@ -124,7 +126,7 @@ void MaterialBaker::processMaterial() { return; } - QPair textureKey = { textureURL, it->second }; + QPair textureKey { textureURL, it->second }; if (!_textureBakers.contains(textureKey)) { QSharedPointer textureBaker { new TextureBaker(textureURL, it->second, _textureOutputDir), diff --git a/libraries/baking/src/baking/FSTBaker.h b/libraries/baking/src/baking/FSTBaker.h index aeb7286af3..85c7c93a37 100644 --- a/libraries/baking/src/baking/FSTBaker.h +++ b/libraries/baking/src/baking/FSTBaker.h @@ -21,7 +21,7 @@ public: FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); - virtual QUrl getFullOutputMappingURL() const; + virtual QUrl getFullOutputMappingURL() const override; signals: void fstLoaded(); diff --git a/libraries/model-baker/CMakeLists.txt b/libraries/model-baker/CMakeLists.txt index 22c240b487..6c0f220340 100644 --- a/libraries/model-baker/CMakeLists.txt +++ b/libraries/model-baker/CMakeLists.txt @@ -4,4 +4,6 @@ setup_hifi_library() link_hifi_libraries(shared shaders task gpu graphics hfm material-networking) include_hifi_library_headers(networking) include_hifi_library_headers(image) -include_hifi_library_headers(ktx) \ No newline at end of file +include_hifi_library_headers(ktx) + +target_draco() diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 5f8ec3a678..639ab8b948 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -145,7 +145,7 @@ void DomainBaker::loadLocalFile() { } } -void DomainBaker::addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { +void DomainBaker::addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) { // grab a QUrl for the model URL QUrl bakeableModelURL = getBakeableModelURL(url); if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) { @@ -185,7 +185,7 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, QJs } } -void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef) { +void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef) { QString cleanURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString(); auto idx = cleanURL.lastIndexOf('.'); auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : ""; @@ -225,7 +225,7 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i } } -void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef) { +void DomainBaker::addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) { // grab a clean version of the URL without a query or fragment QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); @@ -257,7 +257,7 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, QJ _entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef }); } -void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef) { +void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef) { // grab a clean version of the URL without a query or fragment QString materialData; if (isURL) { diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index c9f5a59672..81f5c345cd 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -73,10 +73,10 @@ private: bool _shouldRebakeOriginals { false }; - void addModelBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); - void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, QJsonValueRef& jsonRef); - void addScriptBaker(const QString& property, const QString& url, QJsonValueRef& jsonRef); - void addMaterialBaker(const QString& property, const QString& data, bool isURL, QJsonValueRef& jsonRef); + void addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef); + void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef); + void addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef); + void addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef); }; #endif // hifi_DomainBaker_h From 27c7bf5c922965b24117f941d2b5111de3f4665a Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 17:02:55 -0700 Subject: [PATCH 053/117] Remove duplicate FBX debug dump --- libraries/baking/src/FBXBaker.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 5e346ab8c8..371a492964 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -33,10 +33,6 @@ #include "ModelBakingLoggingCategory.h" #include "TextureBaker.h" -#ifdef HIFI_DUMP_FBX -#include "FBXToJSON.h" -#endif - FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) : ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) { @@ -50,20 +46,6 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector& dracoMeshes, const std::vector>& dracoMaterialLists) { _hfmModel = hfmModel; - -#ifdef HIFI_DUMP_FBX - { - FBXToJSON fbxToJSON; - fbxToJSON << _rootNode; - QFileInfo modelFile(_originalModelFilePath); - QString outFilename(_bakedOutputDir + "/" + modelFile.completeBaseName() + "_FBX.json"); - QFile jsonFile(outFilename); - if (jsonFile.open(QIODevice::WriteOnly)) { - jsonFile.write(fbxToJSON.str().c_str(), fbxToJSON.str().length()); - jsonFile.close(); - } - } -#endif if (shouldStop()) { return; From 9b3b109d2222058ae686f8b07d7620eccfb7eec6 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 13 Mar 2019 17:09:54 -0700 Subject: [PATCH 054/117] make placename consistent with hostname after domain reset --- libraries/networking/src/AddressManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index f4221e3d49..517daf8ce5 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -834,6 +834,7 @@ bool AddressManager::setDomainInfo(const QUrl& domainURL, LookupTrigger trigger) } _domainURL = domainURL; + _shareablePlaceName.clear(); // clear any current place information _rootPlaceID = QUuid(); From 5b75eb34e855450dc4c4ae90a1d35d736c07dc0b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Wed, 13 Mar 2019 17:26:02 -0700 Subject: [PATCH 055/117] Fix ModelBaker not properly checking if texture file name exists --- libraries/baking/src/FBXBaker.h | 1 - libraries/baking/src/ModelBaker.cpp | 16 ++++++++-------- libraries/baking/src/ModelBaker.h | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 3c95273f8f..257efbe983 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -43,7 +43,6 @@ private: void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); hfm::Model::Pointer _hfmModel; - QHash _textureNameMatchCount; QHash _remappedTexturePaths; bool _pendingErrorEmission { false }; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index d906abca8f..77584beb1b 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -417,10 +417,7 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture // 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 - baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo); - // If two textures have the same URL but are used differently, we need to process them separately - QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); - baseTextureFileName += addMapChannel; + baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo, textureType); _remappedTexturePaths[urlToTexture] = baseTextureFileName; } @@ -631,12 +628,15 @@ void ModelBaker::checkIfTexturesFinished() { } } -QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo) { +QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) { + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); + + QString baseTextureFileName{ textureFileInfo.completeBaseName() + addMapChannel }; + // 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 baseTextureFileName{ textureFileInfo.completeBaseName() }; + auto& nameMatches = _textureNameMatchCount[baseTextureFileName]; if (nameMatches > 0) { // there are already nameMatches texture with this name diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index f1ef6db56d..6ee7511ce3 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -97,7 +97,7 @@ private slots: void handleAbortedTexture(); private: - QString createBaseTextureFileName(const QFileInfo & textureFileInfo); + QString createBaseTextureFileName(const QFileInfo & textureFileInfo, const image::TextureUsage::Type textureType); QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); From aeb56ff22a7b56af8634155fdb10a41437ee2513 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 14 Mar 2019 01:08:11 +0100 Subject: [PATCH 056/117] EntityList -> re-focus the rename field rather then re-selecting the text fully EntityProperties -> ignore selection updates when nothing is changed and window is focused --- scripts/system/html/js/entityList.js | 4 ++-- scripts/system/html/js/entityProperties.js | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 8482591771..b15c4e6703 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -459,7 +459,7 @@ function loaded() { isRenameFieldBeingMoved = true; document.body.appendChild(elRenameInput); // keep the focus - elRenameInput.select(); + elRenameInput.focus(); } } @@ -475,7 +475,7 @@ function loaded() { elCell.innerHTML = ""; elCell.appendChild(elRenameInput); // keep the focus - elRenameInput.select(); + elRenameInput.focus(); isRenameFieldBeingMoved = false; } diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 863168d7fd..f501df7933 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -3325,6 +3325,13 @@ function loaded() { } let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; + + if (!hasSelectedEntityChanged && document.hasFocus()) { + // in case the selection has not changed and we still have focus on the properties page, + // we will ignore the event. + return; + } + let doSelectElement = !hasSelectedEntityChanged; // the event bridge and json parsing handle our avatar id string differently. From 5e430c98c598185ba175a12636cc6d9053b872c7 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 09:43:32 -0700 Subject: [PATCH 057/117] Attempt to fix build warnings --- libraries/baking/src/MaterialBaker.cpp | 6 +++--- .../model-baker/src/model-baker/BuildDracoMeshTask.cpp | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 24d031c39e..57dcde67de 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -109,7 +109,7 @@ void MaterialBaker::processMaterial() { QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); // FIXME: this isn't properly handling bumpMaps or glossMaps - static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP { + static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP({ { graphics::Material::MapChannel::EMISSIVE_MAP, image::TextureUsage::EMISSIVE_TEXTURE }, { graphics::Material::MapChannel::ALBEDO_MAP, image::TextureUsage::ALBEDO_TEXTURE }, { graphics::Material::MapChannel::METALLIC_MAP, image::TextureUsage::METALLIC_TEXTURE }, @@ -118,7 +118,7 @@ void MaterialBaker::processMaterial() { { graphics::Material::MapChannel::OCCLUSION_MAP, image::TextureUsage::OCCLUSION_TEXTURE }, { graphics::Material::MapChannel::LIGHTMAP_MAP, image::TextureUsage::LIGHTMAP_TEXTURE }, { graphics::Material::MapChannel::SCATTERING_MAP, image::TextureUsage::SCATTERING_TEXTURE } - }; + }); auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel); if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) { @@ -126,7 +126,7 @@ void MaterialBaker::processMaterial() { return; } - QPair textureKey { textureURL, it->second }; + QPair textureKey(textureURL, it->second); if (!_textureBakers.contains(textureKey)) { QSharedPointer textureBaker { new TextureBaker(textureURL, it->second, _textureOutputDir), diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index e45b2bf584..46b170fd25 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -16,6 +16,11 @@ #pragma warning( push ) #pragma warning( disable : 4267 ) #endif +// gcc and clang +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#endif #include #include @@ -23,6 +28,9 @@ #ifdef _WIN32 #pragma warning( pop ) #endif +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif #include "ModelBakerLogging.h" #include "ModelMath.h" From 6c9c58c657edc0c5ac83d8aaf59aa3f9f870c32a Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 14 Mar 2019 11:36:40 -0700 Subject: [PATCH 058/117] Remove unneeded wait. --- tools/nitpick/src/TestCreator.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index f45a23e459..bbeef11a1f 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -851,10 +851,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; From 4470cf9ae5d87808ef935abf51a5291fdf9bf06d Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 14 Mar 2019 12:35:54 -0700 Subject: [PATCH 059/117] Marketplace category dropdown UI tweaks --- .../hifi/commerce/marketplace/Marketplace.qml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 0f9e9e3620..c703a0e564 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -391,7 +391,7 @@ Rectangle { top: parent.top topMargin: 100 } - width: parent.width/2 + width: parent.width*2/3 color: hifi.colors.white @@ -431,7 +431,7 @@ Rectangle { anchors.leftMargin: 15 anchors.top:parent.top anchors.bottom: parent.bottom - anchors.left: parent.left + anchors.left: categoryItemCount.right elide: Text.ElideRight text: model.name @@ -445,23 +445,23 @@ Rectangle { anchors { top: parent.top bottom: parent.bottom - topMargin: 5 - bottomMargin: 5 + topMargin: 7 + bottomMargin: 7 leftMargin: 10 rightMargin: 10 - left: categoriesItemText.right + left: parent.left } width: childrenRect.width - color: hifi.colors.blueHighlight + color: hifi.colors.faintGray radius: height/2 RalewaySemiBold { anchors.top: parent.top anchors.bottom: parent.bottom - width: paintedWidth+30 + width: 50 text: model.count - color: hifi.colors.white + color: hifi.colors.lightGrayText horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter size: 16 From 19f856b760bbf71ca032eccc45f797c2f47b8faa Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 12 Mar 2019 15:01:11 -0700 Subject: [PATCH 060/117] Switch Oculus mobile to single draw FBO with multiple color attachments --- .../src/main/assets/shaders/present.frag | 37 +++ .../src/main/assets/shaders/present.vert | 21 ++ .../oculus/OculusMobileActivity.java | 7 +- .../oculusMobile/src/ovr/Framebuffer.cpp | 141 +++++++---- libraries/oculusMobile/src/ovr/Framebuffer.h | 26 +- libraries/oculusMobile/src/ovr/VrHandler.cpp | 232 +++++++++++++++--- 6 files changed, 372 insertions(+), 92 deletions(-) create mode 100644 android/libraries/oculus/src/main/assets/shaders/present.frag create mode 100644 android/libraries/oculus/src/main/assets/shaders/present.vert diff --git a/android/libraries/oculus/src/main/assets/shaders/present.frag b/android/libraries/oculus/src/main/assets/shaders/present.frag new file mode 100644 index 0000000000..4fbec70f57 --- /dev/null +++ b/android/libraries/oculus/src/main/assets/shaders/present.frag @@ -0,0 +1,37 @@ +#version 320 es + +precision highp float; +precision highp sampler2D; + +layout(location = 0) in vec4 vTexCoordLR; + +layout(location = 0) out vec4 FragColorL; +layout(location = 1) out vec4 FragColorR; + +uniform sampler2D sampler; + +// https://software.intel.com/en-us/node/503873 + +// sRGB ====> Linear +vec3 color_sRGBToLinear(vec3 srgb) { + return mix(pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)), srgb / vec3(12.92), vec3(lessThanEqual(srgb, vec3(0.04045)))); +} + +vec4 color_sRGBAToLinear(vec4 srgba) { + return vec4(color_sRGBToLinear(srgba.xyz), srgba.w); +} + +// Linear ====> sRGB +vec3 color_LinearTosRGB(vec3 lrgb) { + return mix(vec3(1.055) * pow(vec3(lrgb), vec3(0.41666)) - vec3(0.055), vec3(lrgb) * vec3(12.92), vec3(lessThan(lrgb, vec3(0.0031308)))); +} + +vec4 color_LinearTosRGBA(vec4 lrgba) { + return vec4(color_LinearTosRGB(lrgba.xyz), lrgba.w); +} + +// FIXME switch to texelfetch for getting from the source texture +void main() { + FragColorL = color_LinearTosRGBA(texture(sampler, vTexCoordLR.xy)); + FragColorR = color_LinearTosRGBA(texture(sampler, vTexCoordLR.zw)); +} diff --git a/android/libraries/oculus/src/main/assets/shaders/present.vert b/android/libraries/oculus/src/main/assets/shaders/present.vert new file mode 100644 index 0000000000..dfd6b1412f --- /dev/null +++ b/android/libraries/oculus/src/main/assets/shaders/present.vert @@ -0,0 +1,21 @@ +#version 320 es + +layout(location = 0) out vec4 vTexCoordLR; + +void main(void) { + const float depth = 0.0; + const vec4 UNIT_QUAD[4] = vec4[4]( + vec4(-1.0, -1.0, depth, 1.0), + vec4(1.0, -1.0, depth, 1.0), + vec4(-1.0, 1.0, depth, 1.0), + vec4(1.0, 1.0, depth, 1.0) + ); + vec4 pos = UNIT_QUAD[gl_VertexID]; + gl_Position = pos; + vTexCoordLR.xy = pos.xy; + vTexCoordLR.xy += 1.0; + vTexCoordLR.y *= 0.5; + vTexCoordLR.x *= 0.25; + vTexCoordLR.zw = vTexCoordLR.xy; + vTexCoordLR.z += 0.5; +} diff --git a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java index 8ee22749c9..19865e7751 100644 --- a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java +++ b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java @@ -7,6 +7,7 @@ // package io.highfidelity.oculus; +import android.content.res.AssetManager; import android.os.Bundle; import android.util.Log; import android.view.Surface; @@ -24,7 +25,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca private static final String TAG = OculusMobileActivity.class.getSimpleName(); static { System.loadLibrary("oculusMobile"); } - private native void nativeOnCreate(); + private native void nativeOnCreate(AssetManager assetManager); private native static void nativeOnResume(); private native static void nativeOnPause(); private native static void nativeOnSurfaceChanged(Surface s); @@ -53,7 +54,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca mView = new SurfaceView(this); mView.getHolder().addCallback(this); - nativeOnCreate(); + nativeOnCreate(getAssets()); questNativeOnCreate(); } @@ -81,7 +82,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca Log.w(TAG, "QQQ onResume"); super.onResume(); //Reconnect the global reference back to handler - nativeOnCreate(); + nativeOnCreate(getAssets()); questNativeOnResume(); nativeOnResume(); diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.cpp b/libraries/oculusMobile/src/ovr/Framebuffer.cpp index 0f59eef614..57c45d3159 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.cpp +++ b/libraries/oculusMobile/src/ovr/Framebuffer.cpp @@ -7,59 +7,44 @@ // #include "Framebuffer.h" +#include + #include -#include #include #include #include +#include "Helpers.h" + using namespace ovr; void Framebuffer::updateLayer(int eye, ovrLayerProjection2& layer, const ovrMatrix4f* projectionMatrix ) const { auto& layerTexture = layer.Textures[eye]; - layerTexture.ColorSwapChain = _swapChain; - layerTexture.SwapChainIndex = _index; + layerTexture.ColorSwapChain = _swapChainInfos[eye].swapChain; + layerTexture.SwapChainIndex = _swapChainInfos[eye].index; if (projectionMatrix) { layerTexture.TexCoordsFromTanAngles = ovrMatrix4f_TanAngleMatrixFromProjection( projectionMatrix ); } layerTexture.TextureRect = { 0, 0, 1, 1 }; } +void Framebuffer::SwapChainInfo::destroy() { + if (swapChain != nullptr) { + vrapi_DestroyTextureSwapChain(swapChain); + swapChain = nullptr; + } + index = -1; + length = -1; +} + void Framebuffer::create(const glm::uvec2& size) { _size = size; - _index = 0; - _validTexture = false; - - // Depth renderbuffer - /* glGenRenderbuffers(1, &_depth); - glBindRenderbuffer(GL_RENDERBUFFER, _depth); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, _size.x, _size.y); - glBindRenderbuffer(GL_RENDERBUFFER, 0); -*/ - // Framebuffer - glGenFramebuffers(1, &_fbo); - // glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - // glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depth); - // glBindFramebuffer(GL_FRAMEBUFFER, 0); - - _swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, _size.x, _size.y, 1, 3); - - _length = vrapi_GetTextureSwapChainLength(_swapChain); - if (!_length) { - __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); - return; - } - - for (int i = 0; i < _length; ++i) { - GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, i); - glBindTexture(GL_TEXTURE_2D, chainTexId); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].create(size); + }); glBindTexture(GL_TEXTURE_2D, 0); + glGenFramebuffers(1, &_fbo); } void Framebuffer::destroy() { @@ -67,28 +52,80 @@ void Framebuffer::destroy() { glDeleteFramebuffers(1, &_fbo); _fbo = 0; } - if (0 != _depth) { - glDeleteRenderbuffers(1, &_depth); - _depth = 0; - } - if (_swapChain != nullptr) { - vrapi_DestroyTextureSwapChain(_swapChain); - _swapChain = nullptr; - } - _index = -1; - _length = -1; + + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].destroy(); + }); } void Framebuffer::advance() { - _index = (_index + 1) % _length; - _validTexture = false; + ovr::for_each_eye([&](ovrEye eye) { + _swapChainInfos[eye].advance(); + }); } -void Framebuffer::bind() { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - if (!_validTexture) { - GLuint chainTexId = vrapi_GetTextureSwapChainHandle(_swapChain, _index); - glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, chainTexId, 0); - _validTexture = true; +void Framebuffer::bind(GLenum target) { + glBindFramebuffer(target, _fbo); + _swapChainInfos[0].bind(target, GL_COLOR_ATTACHMENT0); + _swapChainInfos[1].bind(target, GL_COLOR_ATTACHMENT1); +} + +void Framebuffer::invalidate(GLenum target) { + static const std::array INVALIDATE_ATTACHMENTS {{ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }}; + glInvalidateFramebuffer(target, static_cast(INVALIDATE_ATTACHMENTS.size()), INVALIDATE_ATTACHMENTS.data()); +} + + +void Framebuffer::drawBuffers(ovrEye eye) const { + static const std::array, 3> EYE_DRAW_BUFFERS { { + {GL_COLOR_ATTACHMENT0, GL_NONE}, + {GL_NONE, GL_COLOR_ATTACHMENT1}, + {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1} + } }; + + switch(eye) { + case VRAPI_EYE_LEFT: + case VRAPI_EYE_RIGHT: + case VRAPI_EYE_COUNT: { + const auto& eyeDrawBuffers = EYE_DRAW_BUFFERS[eye]; + glDrawBuffers(static_cast(eyeDrawBuffers.size()), eyeDrawBuffers.data()); + } + break; + + default: + throw std::runtime_error("Invalid eye for drawBuffers"); + } +} + +void Framebuffer::SwapChainInfo::create(const glm::uvec2 &size) { + index = 0; + validTexture = false; + swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, size.x, size.y, 1, 3); + length = vrapi_GetTextureSwapChainLength(swapChain); + if (!length) { + __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); + throw std::runtime_error("Unable to create Oculus texture swap chain"); + } + + for (int i = 0; i < length; ++i) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(swapChain, i); + glBindTexture(GL_TEXTURE_2D, chainTexId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } +} + +void Framebuffer::SwapChainInfo::advance() { + index = (index + 1) % length; + validTexture = false; +} + +void Framebuffer::SwapChainInfo::bind(uint32_t target, uint32_t attachment) { + if (!validTexture) { + GLuint chainTexId = vrapi_GetTextureSwapChainHandle(swapChain, index); + glFramebufferTexture(target, attachment, chainTexId, 0); + validTexture = true; } } diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.h b/libraries/oculusMobile/src/ovr/Framebuffer.h index 5127574462..4600d91534 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.h +++ b/libraries/oculusMobile/src/ovr/Framebuffer.h @@ -9,6 +9,7 @@ #include #include +#include #include @@ -20,15 +21,28 @@ public: void create(const glm::uvec2& size); void advance(); void destroy(); - void bind(); + void bind(GLenum target = GL_DRAW_FRAMEBUFFER); + void invalidate(GLenum target = GL_DRAW_FRAMEBUFFER); + void drawBuffers(ovrEye eye) const; - uint32_t _depth { 0 }; + const glm::uvec2& size() const { return _size; } + +private: uint32_t _fbo{ 0 }; - int _length{ -1 }; - int _index{ -1 }; - bool _validTexture{ false }; glm::uvec2 _size; - ovrTextureSwapChain* _swapChain{ nullptr }; + struct SwapChainInfo { + int length{ -1 }; + int index{ -1 }; + bool validTexture{ false }; + ovrTextureSwapChain* swapChain{ nullptr }; + + void create(const glm::uvec2& size); + void destroy(); + void advance(); + void bind(GLenum target, GLenum attachment); + }; + + SwapChainInfo _swapChainInfos[VRAPI_FRAME_LAYER_EYE_MAX]; }; } // namespace ovr \ No newline at end of file diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index 6cc2ec9526..8748ec83cb 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -9,37 +9,186 @@ #include #include +#include +#include #include +#include +#include #include #include #include //#include + #include "GLContext.h" #include "Helpers.h" #include "Framebuffer.h" +static AAssetManager* ASSET_MANAGER = nullptr; +#define USE_BLIT_PRESENT 0 + +#if !USE_BLIT_PRESENT + + + +static std::string getTextAsset(const char* assetPath) { + if (!ASSET_MANAGER || !assetPath) { + return nullptr; + } + AAsset* asset = AAssetManager_open(ASSET_MANAGER, assetPath, AASSET_MODE_BUFFER); + if (!asset) { + return {}; + } + + auto length = AAsset_getLength(asset); + if (0 == length) { + AAsset_close(asset); + return {}; + } + + auto buffer = AAsset_getBuffer(asset); + if (!buffer) { + AAsset_close(asset); + return {}; + } + + std::string result { static_cast(buffer), static_cast(length) }; + AAsset_close(asset); + return result; +} + +static std::string getShaderInfoLog(GLuint glshader) { + std::string result; + GLint infoLength = 0; + glGetShaderiv(glshader, GL_INFO_LOG_LENGTH, &infoLength); + if (infoLength > 0) { + char* temp = new char[infoLength]; + glGetShaderInfoLog(glshader, infoLength, NULL, temp); + result = std::string(temp); + delete[] temp; + } + return result; +} + +static GLuint buildShader(GLenum shaderDomain, const char* shader) { + GLuint glshader = glCreateShader(shaderDomain); + if (!glshader) { + throw std::runtime_error("Bad shader"); + } + + glShaderSource(glshader, 1, &shader, NULL); + glCompileShader(glshader); + + GLint compiled = 0; + glGetShaderiv(glshader, GL_COMPILE_STATUS, &compiled); + + // if compilation fails + if (!compiled) { + std::string compileError = getShaderInfoLog(glshader); + glDeleteShader(glshader); + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "Shader compile error: %s", compileError.c_str()); + return 0; + } + + return glshader; +} + +static std::string getProgramInfoLog(GLuint glprogram) { + std::string result; + GLint infoLength = 0; + glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLength); + if (infoLength > 0) { + char* temp = new char[infoLength]; + glGetProgramInfoLog(glprogram, infoLength, NULL, temp); + result = std::string(temp); + delete[] temp; + } + return result; +} + +static GLuint buildProgram(const char* vertex, const char* fragment) { + // A brand new program: + GLuint glprogram { 0 }, glvertex { 0 }, glfragment { 0 }; + + try { + glprogram = glCreateProgram(); + if (0 == glprogram) { + throw std::runtime_error("Failed to create program, is GL context current?"); + } + + glvertex = buildShader(GL_VERTEX_SHADER, vertex); + if (0 == glvertex) { + throw std::runtime_error("Failed to create or compile vertex shader"); + } + glAttachShader(glprogram, glvertex); + + glfragment = buildShader(GL_FRAGMENT_SHADER, fragment); + if (0 == glfragment) { + throw std::runtime_error("Failed to create or compile fragment shader"); + } + glAttachShader(glprogram, glfragment); + + GLint linked { 0 }; + glLinkProgram(glprogram); + glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); + + if (!linked) { + std::string linkErrorLog = getProgramInfoLog(glprogram); + __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "Program link error: %s", linkErrorLog.c_str()); + throw std::runtime_error("Failed to link program, is the interface between the fragment and vertex shaders correct?"); + } + + + } catch(const std::runtime_error& error) { + if (0 != glprogram) { + glDeleteProgram(glprogram); + glprogram = 0; + } + } + + if (0 != glvertex) { + glDeleteShader(glvertex); + } + + if (0 != glfragment) { + glDeleteShader(glfragment); + } + + if (0 == glprogram) { + throw std::runtime_error("Failed to build program"); + } + + return glprogram; +} + +#endif using namespace ovr; static thread_local bool isRenderThread { false }; struct VrSurface : public TaskQueue { - using HandlerTask = VrHandler::HandlerTask; + using HandlerTask = ovr::VrHandler::HandlerTask; JavaVM* vm{nullptr}; jobject oculusActivity{ nullptr }; ANativeWindow* nativeWindow{ nullptr }; - VrHandler* handler{nullptr}; + ovr::VrHandler* handler{nullptr}; ovrMobile* session{nullptr}; bool resumed { false }; - GLContext vrglContext; - Framebuffer eyeFbos[2]; - uint32_t readFbo{0}; + ovr::GLContext vrglContext; + ovr::Framebuffer eyesFbo; + +#if USE_BLIT_PRESENT + GLuint readFbo { 0 }; +#else + GLuint renderProgram { 0 }; + GLuint renderVao { 0 }; +#endif std::atomic presentIndex{1}; double displayTime{0}; // Not currently set by anything @@ -76,6 +225,16 @@ struct VrSurface : public TaskQueue { vrglContext.create(currentDisplay, currentContext, noErrorContext); vrglContext.makeCurrent(); +#if USE_BLIT_PRESENT + glGenFramebuffers(1, &readFbo); +#else + glGenVertexArrays(1, &renderVao); + const char* vertex = nullptr; + auto vertexShader = getTextAsset("shaders/present.vert"); + auto fragmentShader = getTextAsset("shaders/present.frag"); + renderProgram = buildProgram(vertexShader.c_str(), fragmentShader.c_str()); +#endif + glm::uvec2 eyeTargetSize; withEnv([&](JNIEnv* env){ ovrJava java{ vm, env, oculusActivity }; @@ -85,10 +244,7 @@ struct VrSurface : public TaskQueue { }; }); __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "QQQ Eye Size %d, %d", eyeTargetSize.x, eyeTargetSize.y); - ovr::for_each_eye([&](ovrEye eye) { - eyeFbos[eye].create(eyeTargetSize); - }); - glGenFramebuffers(1, &readFbo); + eyesFbo.create(eyeTargetSize); vrglContext.doneCurrent(); } @@ -178,38 +334,51 @@ struct VrSurface : public TaskQueue { void presentFrame(uint32_t sourceTexture, const glm::uvec2 &sourceSize, const ovrTracking2& tracking) { ovrLayerProjection2 layer = vrapi_DefaultLayerProjection2(); layer.HeadPose = tracking.HeadPose; + + eyesFbo.bind(); if (sourceTexture) { + eyesFbo.invalidate(); +#if USE_BLIT_PRESENT glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo); glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, sourceTexture, 0); - GLenum framebufferStatus = glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); - if (GL_FRAMEBUFFER_COMPLETE != framebufferStatus) { - __android_log_print(ANDROID_LOG_WARN, "QQQ_OVR", "incomplete framebuffer"); - } - } - GLenum invalidateAttachment = GL_COLOR_ATTACHMENT0; - - ovr::for_each_eye([&](ovrEye eye) { - const auto &eyeTracking = tracking.Eye[eye]; - auto &eyeFbo = eyeFbos[eye]; - const auto &destSize = eyeFbo._size; - eyeFbo.bind(); - glInvalidateFramebuffer(GL_DRAW_FRAMEBUFFER, 1, &invalidateAttachment); - if (sourceTexture) { + const auto &destSize = eyesFbo.size(); + ovr::for_each_eye([&](ovrEye eye) { auto sourceWidth = sourceSize.x / 2; auto sourceX = (eye == VRAPI_EYE_LEFT) ? 0 : sourceWidth; + // Each eye blit uses a different draw buffer + eyesFbo.drawBuffers(eye); glBlitFramebuffer( sourceX, 0, sourceX + sourceWidth, sourceSize.y, 0, 0, destSize.x, destSize.y, GL_COLOR_BUFFER_BIT, GL_NEAREST); - } - eyeFbo.updateLayer(eye, layer, &eyeTracking.ProjectionMatrix); - eyeFbo.advance(); - }); - if (sourceTexture) { - glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, &invalidateAttachment); + }); + static const std::array READ_INVALIDATE_ATTACHMENTS {{ GL_COLOR_ATTACHMENT0 }}; + glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, (GLuint)READ_INVALIDATE_ATTACHMENTS.size(), READ_INVALIDATE_ATTACHMENTS.data()); glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 0, 0); +#else + eyesFbo.drawBuffers(VRAPI_EYE_COUNT); + const auto &destSize = eyesFbo.size(); + glViewport(0, 0, destSize.x, destSize.y); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, sourceTexture); + glBindVertexArray(renderVao); + glUseProgram(renderProgram); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glUseProgram(0); + glBindVertexArray(0); +#endif + } else { + eyesFbo.drawBuffers(VRAPI_EYE_COUNT); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); } - glFlush(); + + ovr::for_each_eye([&](ovrEye eye) { + const auto &eyeTracking = tracking.Eye[eye]; + eyesFbo.updateLayer(eye, layer, &eyeTracking.ProjectionMatrix); + }); + + eyesFbo.advance(); ovrLayerHeader2 *layerHeader = &layer.Header; ovrSubmitFrameDescription2 frameDesc = {}; @@ -321,8 +490,9 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { return JNI_VERSION_1_6; } -JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnCreate(JNIEnv* env, jobject obj) { +JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject assetManager) { __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__); + ASSET_MANAGER = AAssetManager_fromJava(env, assetManager); SURFACE.onCreate(env, obj); } From 3016860bab91aa296a2f002a9b1aa03a8b7b3abd Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 13:29:56 -0700 Subject: [PATCH 061/117] Fix QFile::open complaining the device was already open in TextureBaker::processTexture --- libraries/image/src/image/Image.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 6aa09c4d0f..2488b15fcd 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -205,7 +205,11 @@ QImage processRawImageData(QIODevice& content, const std::string& filename) { // Help the QImage loader by extracting the image file format from the url filename ext. // Some tga are not created properly without it. auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - content.open(QIODevice::ReadOnly); + if (!content.isReadable()) { + content.open(QIODevice::ReadOnly); + } else { + content.reset(); + } if (filenameExtension == "tga") { QImage image = image::readTGA(content); From bc696d6db628adca894b9f5fe46920d0ee1cac15 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 14 Mar 2019 13:49:13 -0700 Subject: [PATCH 062/117] fixed memory leak caused by bone length scale computation --- libraries/animation/src/AnimClip.cpp | 66 +++++++++++++++------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 4fe02e9307..1a922e507d 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -124,41 +124,45 @@ void AnimClip::copyFromNetworkAnim() { _anim.resize(animFrameCount); // find the size scale factor for translation in the animation. - const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarSkeleton->nameToJointIndex("Hips")); - const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); - const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarSkeleton->nameToJointIndex("Hips")); - const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animSkeleton.nameToJointIndex("Hips")); - - // the get the units and the heights for the animation and the avatar - const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).y; - const float animationUnitScale = extractScale(animModel.offset).y; - const float avatarHeightInMeters = avatarUnitScale * avatarHipsAbsoluteDefaultPose.trans().y; - const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; - - // get the parent scales for the avatar and the animation - float avatarHipsParentScale = 1.0f; - if (avatarHipsParentIndex >= 0) { - const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); - avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; - } - float animHipsParentScale = 1.0f; - if (animHipsParentIndex >= 0) { - const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); - animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; - } - - const float EPSILON = 0.0001f; float boneLengthScale = 1.0f; - // compute the ratios for the units, the heights in meters, and the parent scales - if ((fabsf(animHeightInMeters) > EPSILON) && (animationUnitScale > EPSILON) && (animHipsParentScale > EPSILON)) { - const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; - const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); - const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); + const int avatarHipsIndex = avatarSkeleton->nameToJointIndex("Hips"); + const int animHipsIndex = animSkeleton.nameToJointIndex("Hips"); + if (avatarHipsIndex != -1 && animHipsIndex != -1) { + const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarHipsIndex); + const int animHipsParentIndex = animSkeleton.getParentIndex(animHipsIndex); - boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsIndex); + const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsIndex); + + // the get the units and the heights for the animation and the avatar + const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).y; + const float animationUnitScale = extractScale(animModel.offset).y; + const float avatarHeightInMeters = avatarUnitScale * avatarHipsAbsoluteDefaultPose.trans().y; + const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; + + // get the parent scales for the avatar and the animation + float avatarHipsParentScale = 1.0f; + if (avatarHipsParentIndex != -1) { + const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); + avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; + } + float animHipsParentScale = 1.0f; + if (animHipsParentIndex != -1) { + const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); + animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; + } + + const float EPSILON = 0.0001f; + // compute the ratios for the units, the heights in meters, and the parent scales + if ((fabsf(animHeightInMeters) > EPSILON) && (animationUnitScale > EPSILON) && (animHipsParentScale > EPSILON)) { + const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; + const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); + const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); + + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + } } - for (int frame = 0; frame < animFrameCount; frame++) { const HFMAnimationFrame& animFrame = animModel.animationFrames[frame]; From fd65f511408c55d616671de94a7ad60c9269f1d0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 14 Mar 2019 14:09:06 -0700 Subject: [PATCH 063/117] Fix gamma correction on Quest --- .../libraries/oculus/src/main/assets/shaders/present.frag | 8 +++++--- libraries/oculusMobile/src/ovr/Framebuffer.cpp | 4 +++- libraries/oculusMobile/src/ovr/VrHandler.cpp | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/android/libraries/oculus/src/main/assets/shaders/present.frag b/android/libraries/oculus/src/main/assets/shaders/present.frag index 4fbec70f57..f5d96932f8 100644 --- a/android/libraries/oculus/src/main/assets/shaders/present.frag +++ b/android/libraries/oculus/src/main/assets/shaders/present.frag @@ -30,8 +30,10 @@ vec4 color_LinearTosRGBA(vec4 lrgba) { return vec4(color_LinearTosRGB(lrgba.xyz), lrgba.w); } -// FIXME switch to texelfetch for getting from the source texture +// FIXME switch to texelfetch for getting from the source texture? void main() { - FragColorL = color_LinearTosRGBA(texture(sampler, vTexCoordLR.xy)); - FragColorR = color_LinearTosRGBA(texture(sampler, vTexCoordLR.zw)); + //FragColorL = color_LinearTosRGBA(texture(sampler, vTexCoordLR.xy)); + //FragColorR = color_LinearTosRGBA(texture(sampler, vTexCoordLR.zw)); + FragColorL = texture(sampler, vTexCoordLR.xy); + FragColorR = texture(sampler, vTexCoordLR.zw); } diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.cpp b/libraries/oculusMobile/src/ovr/Framebuffer.cpp index 57c45d3159..a1dc1841de 100644 --- a/libraries/oculusMobile/src/ovr/Framebuffer.cpp +++ b/libraries/oculusMobile/src/ovr/Framebuffer.cpp @@ -100,7 +100,9 @@ void Framebuffer::drawBuffers(ovrEye eye) const { void Framebuffer::SwapChainInfo::create(const glm::uvec2 &size) { index = 0; validTexture = false; - swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, size.x, size.y, 1, 3); + // GL_SRGB8_ALPHA8 and GL_RGBA8 appear to behave the same here. The only thing that changes the + // output gamma behavior is VRAPI_MODE_FLAG_FRONT_BUFFER_SRGB passed to vrapi_EnterVrMode + swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_SRGB8_ALPHA8, size.x, size.y, 1, 3); length = vrapi_GetTextureSwapChainLength(swapChain); if (!length) { __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures"); diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index 8748ec83cb..a7b0f9f8ee 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -313,6 +313,7 @@ struct VrSurface : public TaskQueue { ovrJava java{ vm, env, oculusActivity }; ovrModeParms modeParms = vrapi_DefaultModeParms(&java); modeParms.Flags |= VRAPI_MODE_FLAG_NATIVE_WINDOW; + modeParms.Flags |= VRAPI_MODE_FLAG_FRONT_BUFFER_SRGB; if (noErrorContext) { modeParms.Flags |= VRAPI_MODE_FLAG_CREATE_CONTEXT_NO_ERROR; } From c985fc735de4f28bcb70f487b94eb39bc8aff1aa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 14 Mar 2019 14:43:43 -0700 Subject: [PATCH 064/117] clean up avatar rendering and ring gizmo normals --- .../src/avatars-renderer/Avatar.cpp | 54 ------------------- .../src/avatars-renderer/Avatar.h | 5 -- .../src/avatars-renderer/SkeletonModel.cpp | 16 +++--- libraries/render-utils/src/GeometryCache.cpp | 6 +-- 4 files changed, 9 insertions(+), 72 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index f3e671143b..46a810f6a4 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1661,60 +1661,6 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) { return bytesRead; } -int Avatar::_jointConesID = GeometryCache::UNKNOWN_ID; - -// render a makeshift cone section that serves as a body part connecting joint spheres -void Avatar::renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, - float radius1, float radius2, const glm::vec4& color) { - - auto geometryCache = DependencyManager::get(); - - if (_jointConesID == GeometryCache::UNKNOWN_ID) { - _jointConesID = geometryCache->allocateID(); - } - - glm::vec3 axis = position2 - position1; - float length = glm::length(axis); - - if (length > 0.0f) { - - axis /= length; - - glm::vec3 perpSin = glm::vec3(1.0f, 0.0f, 0.0f); - glm::vec3 perpCos = glm::normalize(glm::cross(axis, perpSin)); - perpSin = glm::cross(perpCos, axis); - - float angleb = 0.0f; - QVector points; - - for (int i = 0; i < NUM_BODY_CONE_SIDES; i ++) { - - // the rectangles that comprise the sides of the cone section are - // referenced by "a" and "b" in one dimension, and "1", and "2" in the other dimension. - int anglea = angleb; - angleb = ((float)(i+1) / (float)NUM_BODY_CONE_SIDES) * TWO_PI; - - float sa = sinf(anglea); - float sb = sinf(angleb); - float ca = cosf(anglea); - float cb = cosf(angleb); - - glm::vec3 p1a = position1 + perpSin * sa * radius1 + perpCos * ca * radius1; - glm::vec3 p1b = position1 + perpSin * sb * radius1 + perpCos * cb * radius1; - glm::vec3 p2a = position2 + perpSin * sa * radius2 + perpCos * ca * radius2; - glm::vec3 p2b = position2 + perpSin * sb * radius2 + perpCos * cb * radius2; - - points << p1a << p1b << p2a << p1b << p2a << p2b; - } - - PROFILE_RANGE_BATCH(batch, __FUNCTION__); - // TODO: this is really inefficient constantly recreating these vertices buffers. It would be - // better if the avatars cached these buffers for each of the joints they are rendering - geometryCache->updateVertices(_jointConesID, points, color); - geometryCache->renderVertices(batch, gpu::TRIANGLES, _jointConesID); - } -} - float Avatar::getSkeletonHeight() const { Extents extents = _skeletonModel->getBindExtents(); return extents.maximum.y - extents.minimum.y; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 6c31f9fc93..d81b04d4b2 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -296,9 +296,6 @@ public: virtual int parseDataFromBuffer(const QByteArray& buffer) override; - static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, - float radius1, float radius2, const glm::vec4& color); - /**jsdoc * Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, * with an offset of { x: 0, y: 0.1, z: 0 }, your avatar will appear to be raised off the ground slightly. @@ -665,8 +662,6 @@ protected: AvatarTransit _transit; std::mutex _transitLock; - static int _jointConesID; - int _voiceSphereID; float _displayNameTargetAlpha { 1.0f }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index ea71ff128c..fbcf36a8c9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -338,24 +338,20 @@ void SkeletonModel::computeBoundingShape() { void SkeletonModel::renderBoundingCollisionShapes(RenderArgs* args, gpu::Batch& batch, float scale, float alpha) { auto geometryCache = DependencyManager::get(); // draw a blue sphere at the capsule top point - glm::vec3 topPoint = _translation + getRotation() * (scale * (_boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * Vectors::UNIT_Y)); - + glm::vec3 topPoint = _translation + _rotation * (scale * (_boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * Vectors::UNIT_Y)); batch.setModelTransform(Transform().setTranslation(topPoint).postScale(scale * _boundingCapsuleRadius)); geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.6f, 0.6f, 0.8f, alpha)); // draw a yellow sphere at the capsule bottom point - glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, scale * _boundingCapsuleHeight, 0.0f); - glm::vec3 axis = topPoint - bottomPoint; - + glm::vec3 bottomPoint = topPoint - _rotation * glm::vec3(0.0f, scale * _boundingCapsuleHeight, 0.0f); batch.setModelTransform(Transform().setTranslation(bottomPoint).postScale(scale * _boundingCapsuleRadius)); geometryCache->renderSolidSphereInstance(args, batch, glm::vec4(0.8f, 0.8f, 0.6f, alpha)); // draw a green cylinder between the two points - glm::vec3 origin(0.0f); - batch.setModelTransform(Transform().setTranslation(bottomPoint)); - geometryCache->bindSimpleProgram(batch); - Avatar::renderJointConnectingCone(batch, origin, axis, scale * _boundingCapsuleRadius, scale * _boundingCapsuleRadius, - glm::vec4(0.6f, 0.8f, 0.6f, alpha)); + float capsuleDiameter = 2.0f * _boundingCapsuleRadius; + glm::vec3 cylinderDimensions = glm::vec3(capsuleDiameter, _boundingCapsuleHeight, capsuleDiameter); + batch.setModelTransform(Transform().setScale(scale * cylinderDimensions).setRotation(_rotation).setTranslation(0.5f * (topPoint + bottomPoint))); + geometryCache->renderSolidShapeInstance(args, batch, GeometryCache::Shape::Cylinder, glm::vec4(0.6f, 0.8f, 0.6f, alpha)); } bool SkeletonModel::hasSkeleton() { diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index e322dc9d2b..0f400e00ee 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1029,7 +1029,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); auto pointCount = points.size(); auto colorCount = colors.size(); int compactColor = 0; @@ -1107,7 +1107,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); auto pointCount = points.size(); auto colorCount = colors.size(); for (auto i = 0; i < pointCount; i++) { @@ -1195,7 +1195,7 @@ void GeometryCache::updateVertices(int id, const QVector& points, con int* colorData = new int[details.vertices]; int* colorDataAt = colorData; - const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f); + const glm::vec3 NORMAL(0.0f, 1.0f, 0.0f); for (int i = 0; i < points.size(); i++) { glm::vec3 point = points[i]; glm::vec2 texCoord = texCoords[i]; From f9f2b6f8ac5220068c3ec68d66f53e1cb1b981f2 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 14 Mar 2019 15:03:33 -0700 Subject: [PATCH 065/117] avatar exporter 0.3.4/0.3.5 changes to master --- .../{ => AvatarExporter}/AvatarExporter.cs | 767 ++++++--- .../Assets/Editor/AvatarExporter/Average.mat | 76 + .../Assets/Editor/AvatarExporter/Floor.mat | 76 + .../AvatarExporter/HeightReference.prefab | 1393 +++++++++++++++++ .../Assets/Editor/AvatarExporter/Line.mat | 76 + .../Editor/AvatarExporter/ShortOrTall.mat | 76 + .../Editor/AvatarExporter/TooShortOrTall.mat | 76 + tools/unity-avatar-exporter/Assets/README.txt | 13 +- .../avatarExporter.unitypackage | Bin 16045 -> 74582 bytes 9 files changed, 2296 insertions(+), 257 deletions(-) rename tools/unity-avatar-exporter/Assets/Editor/{ => AvatarExporter}/AvatarExporter.cs (67%) create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat create mode 100644 tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs similarity index 67% rename from tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs rename to tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index c25a962824..142e4ae35a 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -6,15 +6,18 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -using UnityEngine; using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; using System; -using System.IO; using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.3"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.5"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -22,6 +25,9 @@ class AvatarExporter : MonoBehaviour { static readonly string EMPTY_WARNING_TEXT = "None"; static readonly string TEXTURES_DIRECTORY = "textures"; static readonly string DEFAULT_MATERIAL_NAME = "No Name"; + static readonly string HEIGHT_REFERENCE_PREFAB = "Assets/Editor/AvatarExporter/HeightReference.prefab"; + static readonly Vector3 PREVIEW_CAMERA_PIVOT = new Vector3(0.0f, 1.755f, 0.0f); + static readonly Vector3 PREVIEW_CAMERA_DIRECTION = new Vector3(0.0f, 0.0f, -1.0f); // TODO: use regex static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { @@ -298,18 +304,17 @@ class AvatarExporter : MonoBehaviour { if (!string.IsNullOrEmpty(occlusionMap)) { json += "\"occlusionMap\": \"" + occlusionMap + "\", "; } - json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "] "; + json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "]"; if (!string.IsNullOrEmpty(emissiveMap)) { - json += "\", emissiveMap\": \"" + emissiveMap + "\""; + json += ", \"emissiveMap\": \"" + emissiveMap + "\""; } - json += "} }"; + json += " } }"; return json; } } static string assetPath = ""; - static string assetName = ""; - + static string assetName = ""; static ModelImporter modelImporter; static HumanDescription humanDescription; @@ -317,12 +322,23 @@ class AvatarExporter : MonoBehaviour { static Dictionary humanoidToUserBoneMappings = new Dictionary(); static BoneTreeNode userBoneTree = new BoneTreeNode(); static Dictionary failedAvatarRules = new Dictionary(); + static string warnings = ""; static Dictionary textureDependencies = new Dictionary(); static Dictionary materialMappings = new Dictionary(); static Dictionary materialDatas = new Dictionary(); - static List materialAlternateStandardShader = new List(); - static Dictionary materialUnsupportedShader = new Dictionary(); + static List alternateStandardShaderMaterials = new List(); + static List unsupportedShaderMaterials = new List(); + + static Scene previewScene; + static string previousScene = ""; + static Vector3 previousScenePivot = Vector3.zero; + static Quaternion previousSceneRotation = Quaternion.identity; + static float previousSceneSize = 0.0f; + static bool previousSceneOrthographic = false; + static UnityEngine.Object avatarResource; + static GameObject avatarPreviewObject; + static GameObject heightReferenceObject; [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { @@ -339,8 +355,8 @@ class AvatarExporter : MonoBehaviour { EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok"); } - static void ExportSelectedAvatar(bool updateAvatar) { - // ensure everything is saved to file before exporting + static void ExportSelectedAvatar(bool updateExistingAvatar) { + // ensure everything is saved to file before doing anything AssetDatabase.SaveAssets(); string[] guids = Selection.assetGUIDs; @@ -364,6 +380,11 @@ class AvatarExporter : MonoBehaviour { " the Rig section of it's Inspector window.", "Ok"); return; } + + avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); + humanDescription = modelImporter.humanDescription; + + string textureWarnings = SetTextureDependencies(); // if the rig is optimized we should de-optimize it during the export process bool shouldDeoptimizeGameObjects = modelImporter.optimizeGameObjects; @@ -371,28 +392,23 @@ class AvatarExporter : MonoBehaviour { modelImporter.optimizeGameObjects = false; modelImporter.SaveAndReimport(); } - - humanDescription = modelImporter.humanDescription; - string textureWarnings = SetTextureDependencies(); + SetBoneAndMaterialInformation(); + if (shouldDeoptimizeGameObjects) { + // switch back to optimized game object in case it was originally optimized + modelImporter.optimizeGameObjects = true; + modelImporter.SaveAndReimport(); + } + // check if we should be substituting a bone for a missing UpperChest mapping AdjustUpperChestMapping(); // format resulting avatar rule failure strings // consider export-blocking avatar rules to be errors and show them in an error dialog, // and also include any other avatar rule failures plus texture warnings as warnings in the dialog - if (shouldDeoptimizeGameObjects) { - // switch back to optimized game object in case it was originally optimized - modelImporter.optimizeGameObjects = true; - modelImporter.SaveAndReimport(); - } - - // format resulting bone rule failure strings - // consider export-blocking bone rules to be errors and show them in an error dialog, - // and also include any other bone rule failures plus texture warnings as warnings in the dialog string boneErrors = ""; - string warnings = ""; + warnings = ""; foreach (var failedAvatarRule in failedAvatarRules) { if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0) { boneErrors += failedAvatarRule.Value + "\n\n"; @@ -400,15 +416,16 @@ class AvatarExporter : MonoBehaviour { warnings += failedAvatarRule.Value + "\n\n"; } } - foreach (string materialName in materialAlternateStandardShader) { - warnings += "The material " + materialName + " is not using the recommended variation of the Standard shader. " + - "We recommend you change it to Standard (Roughness setup) shader for improved performance.\n\n"; - } - foreach (var material in materialUnsupportedShader) { - warnings += "The material " + material.Key + " is using an unsupported shader " + material.Value + - ". Please change it to a Standard shader type.\n\n"; - } + + // add material and texture warnings after bone-related warnings + AddMaterialWarnings(); warnings += textureWarnings; + + // remove trailing newlines at the end of the warnings + if (!string.IsNullOrEmpty(warnings)) { + warnings = warnings.Substring(0, warnings.LastIndexOf("\n\n")); + } + if (!string.IsNullOrEmpty(boneErrors)) { // if there are both errors and warnings then warnings will be displayed with errors in the error dialog if (!string.IsNullOrEmpty(warnings)) { @@ -421,150 +438,157 @@ class AvatarExporter : MonoBehaviour { return; } + // since there are no errors we can now open the preview scene in place of the user's scene + if (!OpenPreviewScene()) { + return; + } + + // show None instead of blank warnings if there are no warnings in the export windows + if (string.IsNullOrEmpty(warnings)) { + warnings = EMPTY_WARNING_TEXT; + } + string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string hifiFolder = documentsFolder + "\\High Fidelity Projects"; - if (updateAvatar) { // Update Existing Avatar menu option - bool copyModelToExport = false; + if (updateExistingAvatar) { // Update Existing Avatar menu option + // open update existing project popup window including project to update, scale, and warnings + // default the initial file chooser location to HiFi projects folder in user documents folder + ExportProjectWindow window = ScriptableObject.CreateInstance(); string initialPath = Directory.Exists(hifiFolder) ? hifiFolder : documentsFolder; - - // open file explorer defaulting to hifi projects folder in user documents to select target fst to update - string exportFstPath = EditorUtility.OpenFilePanel("Select .fst to update", initialPath, "fst"); - if (exportFstPath.Length == 0) { // file selection cancelled - return; - } - exportFstPath = exportFstPath.Replace('/', '\\'); - - // lookup the project name field from the fst file to update - string projectName = ""; - try { - string[] lines = File.ReadAllLines(exportFstPath); - foreach (string line in lines) { - int separatorIndex = line.IndexOf("="); - if (separatorIndex >= 0) { - string key = line.Substring(0, separatorIndex).Trim(); - if (key == "name") { - projectName = line.Substring(separatorIndex + 1).Trim(); - break; - } - } - } - } catch { - EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath + - ". Please check the file and try again.", "Ok"); - return; - } - - string exportModelPath = Path.GetDirectoryName(exportFstPath) + "\\" + assetName + ".fbx"; - if (File.Exists(exportModelPath)) { - // if the fbx in Unity Assets is newer than the fbx in the target export - // folder or vice-versa then ask to replace the older fbx with the newer fbx - DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); - DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); - if (assetModelWriteTime > targetModelWriteTime) { - int option = EditorUtility.DisplayDialogComplex("Error", "The " + assetName + - ".fbx model in the Unity Assets folder is newer than the " + exportModelPath + - " model.\n\nDo you want to replace the older .fbx with the newer .fbx?", - "Yes", "No", "Cancel"); - if (option == 2) { // Cancel - return; - } - copyModelToExport = option == 0; // Yes - } else if (assetModelWriteTime < targetModelWriteTime) { - int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + - " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + - "\n\nDo you want to replace the older .fbx with the newer .fbx and re-import it?", - "Yes", "No" , "Cancel"); - if (option == 2) { // Cancel - return; - } else if (option == 0) { // Yes - copy model to Unity project - // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it - try { - File.Copy(exportModelPath, assetPath, true); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath + - ". Please check the location and try again.", "Ok"); - return; - } - AssetDatabase.ImportAsset(assetPath); - - // set model to Humanoid animation type and force another refresh on it to process Humanoid - modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; - modelImporter.animationType = ModelImporterAnimationType.Human; - EditorUtility.SetDirty(modelImporter); - modelImporter.SaveAndReimport(); - - // redo parent names, joint mappings, and user bone positions due to the fbx change - // as well as re-check the avatar rules for failures - humanDescription = modelImporter.humanDescription; - SetBoneAndMaterialInformation(); - } - } - } else { - // if no matching fbx exists in the target export folder then ask to copy fbx over - int option = EditorUtility.DisplayDialogComplex("Error", "There is no existing " + exportModelPath + - " model.\n\nDo you want to copy over the " + assetName + - ".fbx model from the Unity Assets folder?", "Yes", "No", "Cancel"); - if (option == 2) { // Cancel - return; - } - copyModelToExport = option == 0; // Yes - } - - // copy asset fbx over deleting any existing fbx if we agreed to overwrite it - if (copyModelToExport) { - try { - File.Copy(assetPath, exportModelPath, true); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + assetPath + " to " + exportModelPath + - ". Please check the location and try again.", "Ok"); - return; - } - } - - // delete existing fst file since we will write a new file - // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file - try { - File.Delete(exportFstPath); - } catch { - EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath + - ". Please check the file and try again.", "Ok"); - return; - } - - // write out a new fst file in place of the old file - if (!WriteFST(exportFstPath, projectName)) { - return; - } - - // copy any external texture files to the project's texture directory that are considered dependencies of the model - string texturesDirectory = GetTextureDirectory(exportFstPath); - if (!CopyExternalTextures(texturesDirectory)) { - return; - } - - // display success dialog with any avatar rule warnings - string successDialog = "Avatar successfully updated!"; - if (!string.IsNullOrEmpty(warnings)) { - successDialog += "\n\nWarnings:\n" + warnings; - } - EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); + window.Init(initialPath, warnings, updateExistingAvatar, avatarPreviewObject, OnUpdateExistingProject, OnExportWindowClose); } else { // Export New Avatar menu option // create High Fidelity Projects folder in user documents folder if it doesn't exist if (!Directory.Exists(hifiFolder)) { Directory.CreateDirectory(hifiFolder); } - if (string.IsNullOrEmpty(warnings)) { - warnings = EMPTY_WARNING_TEXT; - } - - // open a popup window to enter new export project name and project location + // open export new project popup window including project name, project location, scale, and warnings + // default the initial project location path to the High Fidelity Projects folder above ExportProjectWindow window = ScriptableObject.CreateInstance(); - window.Init(hifiFolder, warnings, OnExportProjectWindowClose); + window.Init(hifiFolder, warnings, updateExistingAvatar, avatarPreviewObject, OnExportNewProject, OnExportWindowClose); } } - static void OnExportProjectWindowClose(string projectDirectory, string projectName, string warnings) { + static void OnUpdateExistingProject(string exportFstPath, string projectName, float scale) { + bool copyModelToExport = false; + + // lookup the project name field from the fst file to update + projectName = ""; + try { + string[] lines = File.ReadAllLines(exportFstPath); + foreach (string line in lines) { + int separatorIndex = line.IndexOf("="); + if (separatorIndex >= 0) { + string key = line.Substring(0, separatorIndex).Trim(); + if (key == "name") { + projectName = line.Substring(separatorIndex + 1).Trim(); + break; + } + } + } + } catch { + EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + string exportModelPath = Path.GetDirectoryName(exportFstPath) + "\\" + assetName + ".fbx"; + if (File.Exists(exportModelPath)) { + // if the fbx in Unity Assets is newer than the fbx in the target export + // folder or vice-versa then ask to replace the older fbx with the newer fbx + DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); + DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); + if (assetModelWriteTime > targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + assetName + + ".fbx model in the Unity Assets folder is newer than the " + exportModelPath + + " model.\n\nDo you want to replace the older .fbx with the newer .fbx?", + "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; // Yes + } else if (assetModelWriteTime < targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + + " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + + "\n\nDo you want to replace the older .fbx with the newer .fbx and re-import it?", + "Yes", "No" , "Cancel"); + if (option == 2) { // Cancel + return; + } else if (option == 0) { // Yes - copy model to Unity project + // copy the fbx from the project folder to Unity Assets, overwriting the existing fbx, and re-import it + try { + File.Copy(exportModelPath, assetPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + exportModelPath + " to " + assetPath + + ". Please check the location and try again.", "Ok"); + return; + } + AssetDatabase.ImportAsset(assetPath); + + // set model to Humanoid animation type and force another refresh on it to process Humanoid + modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; + modelImporter.animationType = ModelImporterAnimationType.Human; + EditorUtility.SetDirty(modelImporter); + modelImporter.SaveAndReimport(); + + // redo parent names, joint mappings, and user bone positions due to the fbx change + // as well as re-check the avatar rules for failures + humanDescription = modelImporter.humanDescription; + SetBoneAndMaterialInformation(); + } + } + } else { + // if no matching fbx exists in the target export folder then ask to copy fbx over + int option = EditorUtility.DisplayDialogComplex("Error", "There is no existing " + exportModelPath + + " model.\n\nDo you want to copy over the " + assetName + + ".fbx model from the Unity Assets folder?", "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; // Yes + } + + // copy asset fbx over deleting any existing fbx if we agreed to overwrite it + if (copyModelToExport) { + try { + File.Copy(assetPath, exportModelPath, true); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + assetPath + " to " + exportModelPath + + ". Please check the location and try again.", "Ok"); + return; + } + } + + // delete existing fst file since we will write a new file + // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file + try { + File.Delete(exportFstPath); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + // write out a new fst file in place of the old file + if (!WriteFST(exportFstPath, projectName, scale)) { + return; + } + + // copy any external texture files to the project's texture directory that are considered dependencies of the model + string texturesDirectory = GetTextureDirectory(exportFstPath); + if (!CopyExternalTextures(texturesDirectory)) { + return; + } + + // display success dialog with any avatar rule warnings + string successDialog = "Avatar successfully updated!"; + if (!string.IsNullOrEmpty(warnings)) { + successDialog += "\n\nWarnings:\n" + warnings; + } + EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); + } + + static void OnExportNewProject(string projectDirectory, string projectName, float scale) { // copy the fbx from the Unity Assets folder to the project directory string exportModelPath = projectDirectory + assetName + ".fbx"; File.Copy(assetPath, exportModelPath); @@ -577,7 +601,7 @@ class AvatarExporter : MonoBehaviour { // write out the avatar.fst file to the project directory string exportFstPath = projectDirectory + "avatar.fst"; - if (!WriteFST(exportFstPath, projectName)) { + if (!WriteFST(exportFstPath, projectName, scale)) { return; } @@ -592,16 +616,27 @@ class AvatarExporter : MonoBehaviour { if (warnings != EMPTY_WARNING_TEXT) { successDialog += "Warnings:\n" + warnings; } - successDialog += "Note: If you are using any external textures with your model, " + + successDialog += "\n\nNote: If you are using any external textures with your model, " + "please ensure those textures are copied to " + texturesDirectory; EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } + + static void OnExportWindowClose() { + // close the preview avatar scene and go back to user's previous scene when export project windows close + ClosePreviewScene(); + } - static bool WriteFST(string exportFstPath, string projectName) { + // The High Fidelity FBX Serializer omits the colon based prefixes. This will make the jointnames compatible. + static string removeTypeFromJointname(string jointName) { + return jointName.Substring(jointName.IndexOf(':') + 1); + } + + static bool WriteFST(string exportFstPath, string projectName, float scale) { // write out core fields to top of fst file try { - File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + - assetName + ".fbx\n" + "texdir = textures\n"); + File.WriteAllText(exportFstPath, "exporterVersion = " + AVATAR_EXPORTER_VERSION + "\nname = " + projectName + + "\ntype = body+head\nscale = " + scale + "\nfilename = " + assetName + + ".fbx\n" + "texdir = textures\n"); } catch { EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + ". Please check the location and try again.", "Ok"); @@ -612,7 +647,7 @@ class AvatarExporter : MonoBehaviour { foreach (var userBoneInfo in userBoneInfos) { if (userBoneInfo.Value.HasHumanMapping()) { string hifiJointName = HUMANOID_TO_HIFI_JOINT_NAME[userBoneInfo.Value.humanName]; - File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + userBoneInfo.Key + "\n"); + File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + removeTypeFromJointname(userBoneInfo.Key) + "\n"); } } @@ -653,7 +688,7 @@ class AvatarExporter : MonoBehaviour { // swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w); - File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + userBoneName + " = (" + jointOffset.x + ", " + + File.AppendAllText(exportFstPath, "jointRotationOffset2 = " + removeTypeFromJointname(userBoneName) + " = (" + jointOffset.x + ", " + jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n"); } @@ -690,14 +725,13 @@ class AvatarExporter : MonoBehaviour { userBoneTree = new BoneTreeNode(); materialDatas.Clear(); - materialAlternateStandardShader.Clear(); - materialUnsupportedShader.Clear(); - + alternateStandardShaderMaterials.Clear(); + unsupportedShaderMaterials.Clear(); + SetMaterialMappings(); - - // instantiate a game object of the user avatar to traverse the bone tree to gather - // bone parents and positions as well as build a bone tree, then destroy it - UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); + + // instantiate a game object of the user avatar to traverse the bone tree to gather + // bone parents and positions as well as build a bone tree, then destroy it GameObject assetGameObject = (GameObject)Instantiate(avatarResource); TraverseUserBoneTree(assetGameObject.transform); DestroyImmediate(assetGameObject); @@ -732,8 +766,8 @@ class AvatarExporter : MonoBehaviour { bool light = gameObject.GetComponent() != null; bool camera = gameObject.GetComponent() != null; - // if this is a mesh and the model is using external materials then store its material data to be exported - if (mesh && modelImporter.materialLocation == ModelImporterMaterialLocation.External) { + // if this is a mesh then store its material data to be exported if the material is mapped to an fbx material name + if (mesh) { Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials; StoreMaterialData(materials); } else if (!light && !camera) { @@ -959,7 +993,8 @@ class AvatarExporter : MonoBehaviour { string userBoneName = ""; // avatar rule fails if bone is not mapped in Humanoid if (!humanoidToUserBoneMappings.TryGetValue(humanBoneName, out userBoneName)) { - failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + " bone mapped in Humanoid for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + + " bone mapped in Humanoid for the selected avatar."); } return userBoneName; } @@ -1072,11 +1107,11 @@ class AvatarExporter : MonoBehaviour { // don't store any material data for unsupported shader types if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) { - if (!materialUnsupportedShader.ContainsKey(materialName)) { - materialUnsupportedShader.Add(materialName, shaderName); + if (!unsupportedShaderMaterials.Contains(materialName)) { + unsupportedShaderMaterials.Add(materialName); } continue; - } + } MaterialData materialData = new MaterialData(); materialData.albedo = material.GetColor("_Color"); @@ -1100,18 +1135,19 @@ class AvatarExporter : MonoBehaviour { // for non-roughness Standard shaders give a warning that is not the recommended Standard shader, // and invert smoothness for roughness if (shaderName == STANDARD_SHADER || shaderName == STANDARD_SPECULAR_SHADER) { - if (!materialAlternateStandardShader.Contains(materialName)) { - materialAlternateStandardShader.Add(materialName); + if (!alternateStandardShaderMaterials.Contains(materialName)) { + alternateStandardShaderMaterials.Add(materialName); } materialData.roughness = 1.0f - materialData.roughness; } - - // remap the material name from the Unity material name to the fbx material name that it overrides - if (materialMappings.ContainsKey(materialName)) { - materialName = materialMappings[materialName]; - } - if (!materialDatas.ContainsKey(materialName)) { - materialDatas.Add(materialName, materialData); + + // store the material data under each fbx material name that it overrides from the material mapping + foreach (var materialMapping in materialMappings) { + string fbxMaterialName = materialMapping.Key; + string unityMaterialName = materialMapping.Value; + if (unityMaterialName == materialName && !materialDatas.ContainsKey(fbxMaterialName)) { + materialDatas.Add(fbxMaterialName, materialData); + } } } } @@ -1136,20 +1172,110 @@ class AvatarExporter : MonoBehaviour { static void SetMaterialMappings() { materialMappings.Clear(); - // store the mappings from fbx material name to the Unity material name overriding it using external fbx mapping + // store the mappings from fbx material name to the Unity Material name that overrides it using external fbx mapping var objectMap = modelImporter.GetExternalObjectMap(); foreach (var mapping in objectMap) { var material = mapping.Value as UnityEngine.Material; if (material != null) { - materialMappings.Add(material.name, mapping.Key.name); + materialMappings.Add(mapping.Key.name, material.name); } } } + + static void AddMaterialWarnings() { + string alternateStandardShaders = ""; + string unsupportedShaders = ""; + // combine all material names for each material warning into a comma-separated string + foreach (string materialName in alternateStandardShaderMaterials) { + if (!string.IsNullOrEmpty(alternateStandardShaders)) { + alternateStandardShaders += ", "; + } + alternateStandardShaders += materialName; + } + foreach (string materialName in unsupportedShaderMaterials) { + if (!string.IsNullOrEmpty(unsupportedShaders)) { + unsupportedShaders += ", "; + } + unsupportedShaders += materialName; + } + if (alternateStandardShaderMaterials.Count > 1) { + warnings += "The materials " + alternateStandardShaders + " are not using the " + + "recommended variation of the Standard shader. We recommend you change " + + "them to Standard (Roughness setup) shader for improved performance.\n\n"; + } else if (alternateStandardShaderMaterials.Count == 1) { + warnings += "The material " + alternateStandardShaders + " is not using the " + + "recommended variation of the Standard shader. We recommend you change " + + "it to Standard (Roughness setup) shader for improved performance.\n\n"; + } + if (unsupportedShaderMaterials.Count > 1) { + warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " + + "Please change them to a Standard shader type.\n\n"; + } else if (unsupportedShaderMaterials.Count == 1) { + warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " + + "Please change it to a Standard shader type.\n\n"; + } + } + + static bool OpenPreviewScene() { + // see if the user wants to save their current scene before opening preview avatar scene in place of user's scene + if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { + return false; + } + + // store the user's current scene to re-open when done and open a new default scene in place of the user's scene + previousScene = EditorSceneManager.GetActiveScene().path; + previewScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); + + // instantiate a game object to preview the avatar and a game object for the height reference prefab at 0, 0, 0 + UnityEngine.Object heightReferenceResource = AssetDatabase.LoadAssetAtPath(HEIGHT_REFERENCE_PREFAB, typeof(UnityEngine.Object)); + avatarPreviewObject = (GameObject)Instantiate(avatarResource, Vector3.zero, Quaternion.identity); + heightReferenceObject = (GameObject)Instantiate(heightReferenceResource, Vector3.zero, Quaternion.identity); + + // store the camera pivot and rotation from the user's last scene to be restored later + // replace the camera pivot and rotation to point at the preview avatar object in the -Z direction (facing front of it) + var sceneView = SceneView.lastActiveSceneView; + if (sceneView != null) { + previousScenePivot = sceneView.pivot; + previousSceneRotation = sceneView.rotation; + previousSceneSize = sceneView.size; + previousSceneOrthographic = sceneView.orthographic; + sceneView.pivot = PREVIEW_CAMERA_PIVOT; + sceneView.rotation = Quaternion.LookRotation(PREVIEW_CAMERA_DIRECTION); + sceneView.orthographic = true; + sceneView.size = 5.0f; + } + + return true; + } + + static void ClosePreviewScene() { + // destroy the avatar and height reference game objects closing the scene + DestroyImmediate(avatarPreviewObject); + DestroyImmediate(heightReferenceObject); + + // re-open the scene the user had open before switching to the preview scene + if (!string.IsNullOrEmpty(previousScene)) { + EditorSceneManager.OpenScene(previousScene); + } + + // close the preview scene and flag it to be removed + EditorSceneManager.CloseScene(previewScene, true); + + // restore the camera pivot and rotation to the user's previous scene settings + var sceneView = SceneView.lastActiveSceneView; + if (sceneView != null) { + sceneView.pivot = previousScenePivot; + sceneView.rotation = previousSceneRotation; + sceneView.size = previousSceneSize; + sceneView.orthographic = previousSceneOrthographic; + } + } } class ExportProjectWindow : EditorWindow { const int WINDOW_WIDTH = 500; - const int WINDOW_HEIGHT = 460; + const int EXPORT_NEW_WINDOW_HEIGHT = 520; + const int UPDATE_EXISTING_WINDOW_HEIGHT = 465; const int BUTTON_FONT_SIZE = 16; const int LABEL_FONT_SIZE = 16; const int TEXT_FIELD_FONT_SIZE = 14; @@ -1157,28 +1283,62 @@ class ExportProjectWindow : EditorWindow { const int ERROR_FONT_SIZE = 12; const int WARNING_SCROLL_HEIGHT = 170; const string EMPTY_ERROR_TEXT = "None\n"; - + const int SLIDER_WIDTH = 340; + const int SCALE_TEXT_WIDTH = 60; + const float MIN_SCALE_SLIDER = 0.0f; + const float MAX_SCALE_SLIDER = 2.0f; + const int SLIDER_SCALE_EXPONENT = 10; + const float ACTUAL_SCALE_OFFSET = 1.0f; + const float DEFAULT_AVATAR_HEIGHT = 1.755f; + const float MAXIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; + const float MINIMUM_RECOMMENDED_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + readonly Color COLOR_YELLOW = Color.yellow; //new Color(0.9176f, 0.8274f, 0.0f); + readonly Color COLOR_BACKGROUND = new Color(0.5f, 0.5f, 0.5f); + + GameObject avatarPreviewObject; + bool updateExistingAvatar = false; string projectName = ""; string projectLocation = ""; + string initialProjectLocation = ""; string projectDirectory = ""; string errorText = EMPTY_ERROR_TEXT; - string warningText = ""; + string warningText = "\n"; Vector2 warningScrollPosition = new Vector2(0, 0); + string scaleWarningText = ""; + float sliderScale = 0.30103f; - public delegate void OnCloseDelegate(string projectDirectory, string projectName, string warnings); + public delegate void OnExportDelegate(string projectDirectory, string projectName, float scale); + OnExportDelegate onExportCallback; + + public delegate void OnCloseDelegate(); OnCloseDelegate onCloseCallback; - public void Init(string initialPath, string warnings, OnCloseDelegate closeCallback) { - minSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT); - maxSize = new Vector2(WINDOW_WIDTH, WINDOW_HEIGHT); - titleContent.text = "Export New Avatar"; - projectLocation = initialPath; + public void Init(string initialPath, string warnings, bool updateExisting, GameObject avatarObject, + OnExportDelegate exportCallback, OnCloseDelegate closeCallback) { + updateExistingAvatar = updateExisting; + float windowHeight = updateExistingAvatar ? UPDATE_EXISTING_WINDOW_HEIGHT : EXPORT_NEW_WINDOW_HEIGHT; + minSize = new Vector2(WINDOW_WIDTH, windowHeight); + maxSize = new Vector2(WINDOW_WIDTH, windowHeight); + avatarPreviewObject = avatarObject; + titleContent.text = updateExistingAvatar ? "Update Existing Avatar" : "Export New Avatar"; + initialProjectLocation = initialPath; + projectLocation = updateExistingAvatar ? "" : initialProjectLocation; warningText = warnings; + onExportCallback = exportCallback; onCloseCallback = closeCallback; + ShowUtility(); + + // if the avatar's starting height is outside of the recommended ranges, auto-adjust the scale to default height + float height = GetAvatarHeight(); + if (height < MINIMUM_RECOMMENDED_HEIGHT || height > MAXIMUM_RECOMMENDED_HEIGHT) { + float newScale = DEFAULT_AVATAR_HEIGHT / height; + SetAvatarScale(newScale); + scaleWarningText = "Avatar's scale automatically adjusted to be within the recommended range."; + } } - void OnGUI() { + void OnGUI() { // define UI styles for all GUI elements to be created GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); buttonStyle.fontSize = BUTTON_FONT_SIZE; @@ -1192,35 +1352,82 @@ class ExportProjectWindow : EditorWindow { errorStyle.normal.textColor = Color.red; errorStyle.wordWrap = true; GUIStyle warningStyle = new GUIStyle(errorStyle); - warningStyle.normal.textColor = Color.yellow; + warningStyle.normal.textColor = COLOR_YELLOW; + GUIStyle sliderStyle = new GUIStyle(GUI.skin.horizontalSlider); + sliderStyle.fixedWidth = SLIDER_WIDTH; + GUIStyle sliderThumbStyle = new GUIStyle(GUI.skin.horizontalSliderThumb); + + // set the background for the window to a darker gray + Texture2D backgroundTexture = new Texture2D(1, 1, TextureFormat.RGBA32, false); + backgroundTexture.SetPixel(0, 0, COLOR_BACKGROUND); + backgroundTexture.Apply(); + GUI.DrawTexture(new Rect(0, 0, maxSize.x, maxSize.y), backgroundTexture, ScaleMode.StretchToFill); GUILayout.Space(10); - // Project name label and input text field - GUILayout.Label("Export project name:", labelStyle); - projectName = GUILayout.TextField(projectName, textStyle); + if (updateExistingAvatar) { + // Project file to update label and input text field + GUILayout.Label("Project file to update:", labelStyle); + projectLocation = GUILayout.TextField(projectLocation, textStyle); + } else { + // Project name label and input text field + GUILayout.Label("Export project name:", labelStyle); + projectName = GUILayout.TextField(projectName, textStyle); + + GUILayout.Space(10); + + // Project location label and input text field + GUILayout.Label("Export project location:", labelStyle); + projectLocation = GUILayout.TextField(projectLocation, textStyle); + } - GUILayout.Space(10); - - // Project location label and input text field - GUILayout.Label("Export project location:", labelStyle); - projectLocation = GUILayout.TextField(projectLocation, textStyle); - - // Browse button to open folder explorer that starts at project location path and then updates project location + // Browse button to open file/folder explorer and set project location if (GUILayout.Button("Browse", buttonStyle)) { - string result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); - if (result.Length > 0) { // folder selection not cancelled + string result = ""; + if (updateExistingAvatar) { + // open file explorer starting at hifi projects folder in user documents and select target fst to update + string initialPath = string.IsNullOrEmpty(projectLocation) ? initialProjectLocation : projectLocation; + result = EditorUtility.OpenFilePanel("Select .fst to update", initialPath, "fst"); + } else { + // open folder explorer starting at project location path and select folder to create project folder in + result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); + } + if (!string.IsNullOrEmpty(result)) { // file/folder selection not cancelled projectLocation = result.Replace('/', '\\'); } } - // Red error label text to display any file-related errors + // warning if scale is above/below recommended range or if scale was auto-adjusted initially + GUILayout.Label(scaleWarningText, warningStyle); + + // from left to right show scale label, scale slider itself, and scale value input with % value + // slider value itself is from 0.0 to 2.0, and actual scale is an exponent of it with an offset of 1 + // displayed scale is the actual scale value with 2 decimal places, and changing the displayed + // scale via keyboard does the inverse calculation to get the slider value via logarithm + GUILayout.BeginHorizontal(); + GUILayout.Label("Scale:", labelStyle); + sliderScale = GUILayout.HorizontalSlider(sliderScale, MIN_SCALE_SLIDER, MAX_SCALE_SLIDER, sliderStyle, sliderThumbStyle); + float actualScale = (Mathf.Pow(SLIDER_SCALE_EXPONENT, sliderScale) - ACTUAL_SCALE_OFFSET); + GUIStyle scaleInputStyle = new GUIStyle(textStyle); + scaleInputStyle.fixedWidth = SCALE_TEXT_WIDTH; + actualScale *= 100.0f; // convert to 100-based percentage for display purposes + string actualScaleStr = GUILayout.TextField(String.Format("{0:0.00}", actualScale), scaleInputStyle); + actualScaleStr = Regex.Replace(actualScaleStr, @"[^0-9.]", ""); + actualScale = float.Parse(actualScaleStr); + actualScale /= 100.0f; // convert back to 1.0-based percentage + SetAvatarScale(actualScale); + GUILayout.Label("%", labelStyle); + GUILayout.EndHorizontal(); + + GUILayout.Space(15); + + // red error label text to display any file-related errors GUILayout.Label("Error:", errorStyle); GUILayout.Label(errorText, errorStyle); GUILayout.Space(10); - // Yellow warning label text to display scrollable list of any bone-related warnings + // yellow warning label text to display scrollable list of any bone-related warnings GUILayout.Label("Warnings:", warningStyle); warningScrollPosition = GUILayout.BeginScrollView(warningScrollPosition, GUILayout.Width(WINDOW_WIDTH), GUILayout.Height(WARNING_SCROLL_HEIGHT)); @@ -1229,64 +1436,122 @@ class ExportProjectWindow : EditorWindow { GUILayout.Space(10); - // Export button which will verify project folder can actually be created + // export button will verify target project folder can actually be created (or target fst file is valid) // before closing popup window and calling back to initiate the export bool export = false; if (GUILayout.Button("Export", buttonStyle)) { export = true; if (!CheckForErrors(true)) { Close(); - onCloseCallback(projectDirectory, projectName, warningText); + onExportCallback(updateExistingAvatar ? projectLocation : projectDirectory, projectName, actualScale); } } - // Cancel button just closes the popup window without callback + // cancel button closes the popup window triggering the close callback to close the preview scene if (GUILayout.Button("Cancel", buttonStyle)) { Close(); } - // When either text field changes check for any errors if we didn't just check errors from clicking Export above + // when any value changes check for any errors and update scale warning if we are not exporting if (GUI.changed && !export) { CheckForErrors(false); + UpdateScaleWarning(); } } bool CheckForErrors(bool exporting) { errorText = EMPTY_ERROR_TEXT; // default to None if no errors found - projectDirectory = projectLocation + "\\" + projectName + "\\"; - if (projectName.Length > 0) { - // new project must have a unique folder name since the folder will be created for it - if (Directory.Exists(projectDirectory)) { - errorText = "A folder with the name " + projectName + - " already exists at that location.\nPlease choose a different project name or location."; + if (updateExistingAvatar) { + // if any text is set in the project file to update field verify that the file actually exists + if (projectLocation.Length > 0) { + if (!File.Exists(projectLocation)) { + errorText = "Please select a valid project file to update.\n"; + return true; + } + } else if (exporting) { + errorText = "Please select a project file to update.\n"; return true; } - } - if (projectLocation.Length > 0) { - // before clicking Export we can verify that the project location at least starts with a drive - if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { - errorText = "Project location is invalid. Please choose a different project location.\n"; - return true; + } else { + projectDirectory = projectLocation + "\\" + projectName + "\\"; + if (projectName.Length > 0) { + // new project must have a unique folder name since the folder will be created for it + if (Directory.Exists(projectDirectory)) { + errorText = "A folder with the name " + projectName + + " already exists at that location.\nPlease choose a different project name or location."; + return true; + } } - } - if (exporting) { - // when exporting, project name and location must both be defined, and project location must - // be valid and accessible (we attempt to create the project folder at this time to verify this) - if (projectName.Length == 0) { - errorText = "Please define a project name.\n"; - return true; - } else if (projectLocation.Length == 0) { - errorText = "Please define a project location.\n"; - return true; - } else { - try { - Directory.CreateDirectory(projectDirectory); - } catch { + if (projectLocation.Length > 0) { + // before clicking Export we can verify that the project location at least starts with a drive + if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { errorText = "Project location is invalid. Please choose a different project location.\n"; return true; } } - } + if (exporting) { + // when exporting, project name and location must both be defined, and project location must + // be valid and accessible (we attempt to create the project folder at this time to verify this) + if (projectName.Length == 0) { + errorText = "Please define a project name.\n"; + return true; + } else if (projectLocation.Length == 0) { + errorText = "Please define a project location.\n"; + return true; + } else { + try { + Directory.CreateDirectory(projectDirectory); + } catch { + errorText = "Project location is invalid. Please choose a different project location.\n"; + return true; + } + } + } + } + return false; } + + void UpdateScaleWarning() { + // called on any input changes + float height = GetAvatarHeight(); + if (height < MINIMUM_RECOMMENDED_HEIGHT) { + scaleWarningText = "The height of the avatar is below the recommended minimum."; + } else if (height > MAXIMUM_RECOMMENDED_HEIGHT) { + scaleWarningText = "The height of the avatar is above the recommended maximum."; + } else { + scaleWarningText = ""; + } + } + + float GetAvatarHeight() { + // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers + Bounds bounds = new Bounds(); + var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); + var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + foreach (var renderer in skinnedMeshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + return bounds.max.y; + } + + void SetAvatarScale(float actualScale) { + // set the new scale uniformly on the preview avatar's transform to show the resulting avatar size + avatarPreviewObject.transform.localScale = new Vector3(actualScale, actualScale, actualScale); + + // adjust slider scale value to match the new actual scale value + sliderScale = GetSliderScaleFromActualScale(actualScale); + } + + float GetSliderScaleFromActualScale(float actualScale) { + // since actual scale is an exponent of slider scale with an offset, do the logarithm operation to convert it back + return Mathf.Log(actualScale + ACTUAL_SCALE_OFFSET, SLIDER_SCALE_EXPONENT); + } + + void OnDestroy() { + onCloseCallback(); + } } diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat new file mode 100644 index 0000000000..69421ca8e2 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Average.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Average + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.53309965, g: 0.8773585, b: 0.27727836, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat new file mode 100644 index 0000000000..4c63832593 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Floor.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Floor + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab new file mode 100644 index 0000000000..3a6b6b21fa --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab @@ -0,0 +1,1393 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1001 &100100000 +Prefab: + m_ObjectHideFlags: 1 + serializedVersion: 2 + m_Modification: + m_TransformParent: {fileID: 0} + m_Modifications: [] + m_RemovedComponents: [] + m_SourcePrefab: {fileID: 0} + m_RootGameObject: {fileID: 1663253797283788} + m_IsPrefabAsset: 1 +--- !u!1 &1046656866020106 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224386929081752724} + - component: {fileID: 222160789105267064} + - component: {fileID: 114930405832365464} + m_Layer: 5 + m_Name: TwoAndHalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1098451480288840 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4735851023856772} + - component: {fileID: 33008877752475126} + - component: {fileID: 23983268565997994} + m_Layer: 0 + m_Name: HalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1107359137501064 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224352215517075892} + - component: {fileID: 222924084127982026} + - component: {fileID: 114523909969846714} + m_Layer: 5 + m_Name: TwoMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1108041172082256 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224494569551489322} + - component: {fileID: 223961774962398002} + - component: {fileID: 114011556853048752} + - component: {fileID: 114521005238033952} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1165326825168616 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224593141416602104} + - component: {fileID: 222331762946337184} + - component: {fileID: 114101794169638918} + m_Layer: 5 + m_Name: OneAndHalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1182485492886750 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4302978871272126} + - component: {fileID: 33686989621546016} + - component: {fileID: 23982106336197490} + m_Layer: 0 + m_Name: TwoAndHalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1365616260555366 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224613908675679132} + - component: {fileID: 222421911825862480} + - component: {fileID: 114276838631099888} + m_Layer: 5 + m_Name: OneMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1398639835840810 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4460037940915778} + - component: {fileID: 33999849812690240} + - component: {fileID: 23416265009837404} + m_Layer: 0 + m_Name: Floor + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1534720920953066 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4413776654278098} + - component: {fileID: 33291071156168694} + - component: {fileID: 23550720950256080} + m_Layer: 0 + m_Name: Average + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1594624973687270 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4908828994703896} + - component: {fileID: 33726300519449444} + - component: {fileID: 23824769923661608} + m_Layer: 0 + m_Name: Tall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1663253797283788 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4466308008297536} + m_Layer: 0 + m_Name: HeightReference + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1684603522306818 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4359301733271006} + - component: {fileID: 33170278100239952} + - component: {fileID: 23463284742561382} + m_Layer: 0 + m_Name: TwoMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1758516477546936 +GameObject: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 224093314116541246} + - component: {fileID: 222104353024021134} + - component: {fileID: 114198955202599194} + m_Layer: 5 + m_Name: HalfMText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1843086377652878 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4967607462495426} + - component: {fileID: 33458427168817864} + - component: {fileID: 23807848267690204} + m_Layer: 0 + m_Name: Short + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1845490813592506 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4990347338131576} + - component: {fileID: 108630196659418708} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1883639722740524 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4177433262325602} + - component: {fileID: 33418961761515394} + - component: {fileID: 23536779434871182} + m_Layer: 0 + m_Name: TooShort + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1885741171197356 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4718462335765420} + - component: {fileID: 33030310456480364} + - component: {fileID: 23105277758912132} + m_Layer: 0 + m_Name: TooTall + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1919147340747728 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4440944676647488} + - component: {fileID: 33820823812379558} + - component: {fileID: 23886085173153614} + m_Layer: 0 + m_Name: OneAndHalfMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!1 &1985295559338180 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + serializedVersion: 6 + m_Component: + - component: {fileID: 4498194399146796} + - component: {fileID: 33041053251399642} + - component: {fileID: 23936786851965954} + m_Layer: 0 + m_Name: OneMLine + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4177433262325602 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.219375, z: -1} + m_LocalScale: {x: 200, y: 0.43875, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4302978871272126 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 2.5, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 12 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4359301733271006 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 2, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 11 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4413776654278098 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 1.535625, z: -1} + m_LocalScale: {x: 200, y: 1.19375, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4440944676647488 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 1.5, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 10 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4460037940915778 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: -50, z: -0.5} + m_LocalScale: {x: 200, y: 100, z: 2} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4466308008297536 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1663253797283788} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 4990347338131576} + - {fileID: 4460037940915778} + - {fileID: 4177433262325602} + - {fileID: 4967607462495426} + - {fileID: 4413776654278098} + - {fileID: 4908828994703896} + - {fileID: 4718462335765420} + - {fileID: 224494569551489322} + - {fileID: 4735851023856772} + - {fileID: 4498194399146796} + - {fileID: 4440944676647488} + - {fileID: 4359301733271006} + - {fileID: 4302978871272126} + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4498194399146796 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_LocalRotation: {x: 0.7071068, y: -0, z: -0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 1, z: -0.94} + m_LocalScale: {x: 200, y: 1.0000005, z: 0.0100000035} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 9 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4718462335765420 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 502.6325, z: -1} + m_LocalScale: {x: 200, y: 1000, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 6 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4735851023856772 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0.5, z: -0.94} + m_LocalScale: {x: 200, y: 1, z: 0.01} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 8 + m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} +--- !u!4 &4908828994703896 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 2.3825, z: -1} + m_LocalScale: {x: 200, y: 0.5, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 5 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4967607462495426 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0.68875, z: -1} + m_LocalScale: {x: 200, y: 0.5, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!4 &4990347338131576 +Transform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1845490813592506} + m_LocalRotation: {x: -0.11086535, y: -0.8745676, z: 0.40781754, w: -0.23775047} + m_LocalPosition: {x: 0, y: 3, z: 77.17} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4466308008297536} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 50.000004, y: -210.41699, z: 0} +--- !u!23 &23105277758912132 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d07e04b46b88ae54e9f418c8645f1580, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23416265009837404 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 320b570da434d374985fe89d653ae75b, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23463284742561382 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23536779434871182 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d07e04b46b88ae54e9f418c8645f1580, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23550720950256080 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 722779087c41d074eb632820263fc661, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23807848267690204 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 1cd16d030e4890a4cab22d897ccfc8d8, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23824769923661608 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: 1cd16d030e4890a4cab22d897ccfc8d8, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23886085173153614 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23936786851965954 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23982106336197490 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!23 &23983268565997994 +MeshRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RenderingLayerMask: 4294967295 + m_Materials: + - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 0 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 +--- !u!33 &33008877752475126 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1098451480288840} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33030310456480364 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1885741171197356} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33041053251399642 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1985295559338180} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33170278100239952 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1684603522306818} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33291071156168694 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1534720920953066} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33418961761515394 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1883639722740524} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33458427168817864 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1843086377652878} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33686989621546016 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1182485492886750} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33726300519449444 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1594624973687270} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33820823812379558 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1919147340747728} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!33 &33999849812690240 +MeshFilter: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1398639835840810} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!108 &108630196659418708 +Light: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1845490813592506} + m_Enabled: 1 + serializedVersion: 8 + m_Type: 1 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 2 + m_Range: 10 + m_SpotAngle: 30 + m_CookieSize: 10 + m_Shadows: + m_Type: 0 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_Lightmapping: 1 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 +--- !u!114 &114011556853048752 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1980459831, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 +--- !u!114 &114101794169638918 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '1.5m + +' +--- !u!114 &114198955202599194 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: 0.5m +--- !u!114 &114276838631099888 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '1.0m + +' +--- !u!114 &114521005238033952 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 1301386320, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &114523909969846714 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '2.0m + +' +--- !u!114 &114930405832365464 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_RaycastTarget: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI, + Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + m_FontData: + m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0} + m_FontSize: 128 + m_FontStyle: 0 + m_BestFit: 0 + m_MinSize: 0 + m_MaxSize: 256 + m_Alignment: 0 + m_AlignByGeometry: 0 + m_RichText: 1 + m_HorizontalOverflow: 0 + m_VerticalOverflow: 0 + m_LineSpacing: 1 + m_Text: '2.5m + +' +--- !u!222 &222104353024021134 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_CullTransparentMesh: 0 +--- !u!222 &222160789105267064 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_CullTransparentMesh: 0 +--- !u!222 &222331762946337184 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_CullTransparentMesh: 0 +--- !u!222 &222421911825862480 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_CullTransparentMesh: 0 +--- !u!222 &222924084127982026 +CanvasRenderer: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_CullTransparentMesh: 0 +--- !u!223 &223961774962398002 +Canvas: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 2 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_AdditionalShaderChannelsFlag: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &224093314116541246 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1758516477546936} + m_LocalRotation: {x: 0, y: 1, z: 0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.001, y: 0.001, z: 0.001} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 0.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224352215517075892 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1107359137501064} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 2.05} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224386929081752724 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1046656866020106} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 4 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 2.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224494569551489322 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1108041172082256} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 224093314116541246} + - {fileID: 224613908675679132} + - {fileID: 224593141416602104} + - {fileID: 224352215517075892} + - {fileID: 224386929081752724} + m_Father: {fileID: 4466308008297536} + m_RootOrder: 7 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 1000, y: 1000} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224593141416602104 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1165326825168616} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 1.55} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!224 &224613908675679132 +RectTransform: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 100100000} + m_GameObject: {fileID: 1365616260555366} + m_LocalRotation: {x: -0, y: 1, z: -0, w: 0} + m_LocalPosition: {x: 0, y: 0, z: -0.395} + m_LocalScale: {x: 0.0009999999, y: 0.0009999999, z: 0.0009999999} + m_Children: [] + m_Father: {fileID: 224494569551489322} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 180, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -2, y: 1.05} + m_SizeDelta: {x: 300, y: 200} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat new file mode 100644 index 0000000000..2f9a048c63 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/Line.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: Line + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0, g: 0, b: 0, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat new file mode 100644 index 0000000000..5543fef85e --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/ShortOrTall.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: ShortOrTall + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.91758025, g: 0.9622642, b: 0.28595585, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat new file mode 100644 index 0000000000..4851a64056 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/TooShortOrTall.mat @@ -0,0 +1,76 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_Name: TooShortOrTall + m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} + m_ShaderKeywords: + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Floats: + - _BumpScale: 1 + - _Cutoff: 0.5 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _GlossMapScale: 1 + - _Glossiness: 0.5 + - _GlossyReflections: 1 + - _Metallic: 0 + - _Mode: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.02 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _UVSec: 0 + - _ZWrite: 1 + m_Colors: + - _Color: {r: 0.9056604, g: 0.19223925, b: 0.19223925, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 402719b497..767c093800 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.3 +Version 0.3.5 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. @@ -9,15 +9,16 @@ To create a new avatar project: 2. Select the .fbx avatar that you imported in step 1 in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply. 3. With the .fbx avatar still selected in the Assets window, choose High Fidelity menu > Export New Avatar. 4. Select a name for your avatar project (this will be used to create a directory with that name), as well as the target location for your project folder. -5. Once it is exported, your project directory will open in File Explorer. +5. If necessary, adjust the scale for your avatar so that it's height is within the recommended range. +6. Once it is exported, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer. To update an existing avatar project: -1. Select the existing .fbx avatar in the Assets window that you would like to re-export. -2. Choose High Fidelity menu > Update Existing Avatar and browse to the .fst file you would like to update. +1. Select the existing .fbx avatar in the Assets window that you would like to re-export and choose High Fidelity menu > Update Existing Avatar +2. Select the .fst project file that you wish to update. 3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your selected avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file before performing the update. -4. Once it is updated, your project directory will open in File Explorer. +4. Once it is updated, you will receive a successfully exported dialog with any warnings, and your project directory will open in File Explorer. * WARNING * If you are using any external textures as part of your .fbx model, be sure they are copied into the textures folder that is created in the project folder after exporting a new avatar. -For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension +For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension \ No newline at end of file diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index f7385e38311e93788a17308f7cb2a174c7f7f138..ee3f6abe01b509bba63360b2834400660f9f3901 100644 GIT binary patch literal 74582 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 Date: Thu, 14 Mar 2019 13:40:23 -0700 Subject: [PATCH 067/117] Fix baked material JSONS using wrong texture paths --- libraries/baking/src/MaterialBaker.cpp | 8 +++++--- libraries/baking/src/MaterialBaker.h | 3 ++- tools/oven/src/BakerCLI.cpp | 2 +- tools/oven/src/DomainBaker.cpp | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 57dcde67de..2752890f55 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -27,11 +27,12 @@ std::function MaterialBaker::_getNextOvenWorkerThreadOperator; static int materialNum = 0; -MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) : +MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) : _materialData(materialData), _isURL(isURL), _bakedOutputDir(bakedOutputDir), - _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)) + _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)), + _destinationPath(destinationPath) { } @@ -162,10 +163,11 @@ void MaterialBaker::handleFinishedTextureBaker() { qCDebug(material_baking) << "Re-writing texture references to" << baker->getTextureURL(); auto newURL = QUrl(_textureOutputDir).resolved(baker->getMetaTextureFileName()); + auto relativeURL = QDir(_bakedOutputDir).relativeFilePath(newURL.toString()); // Replace the old texture URLs for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) { - networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(newURL); + networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(_destinationPath.resolved(relativeURL)); } } else { // this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index b1678e5634..98f931b61c 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -23,7 +23,7 @@ static const QString BAKED_MATERIAL_EXTENSION = ".baked.json"; class MaterialBaker : public Baker { Q_OBJECT public: - MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir); + MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath); QString getMaterialData() const { return _materialData; } bool isURL() const { return _isURL; } @@ -56,6 +56,7 @@ private: QString _bakedOutputDir; QString _textureOutputDir; QString _bakedMaterialData; + QUrl _destinationPath; QScriptEngine _scriptEngine; static std::function _getNextOvenWorkerThreadOperator; diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index 1aae6ccb72..2946db650c 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -61,7 +61,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& _baker = std::unique_ptr { new JSBaker(inputUrl, outputPath) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else if (type == MATERIAL_EXTENSION) { - _baker = std::unique_ptr { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath) }; + _baker = std::unique_ptr { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath, QUrl(outputPath)) }; _baker->moveToThread(Oven::instance().getNextWorkerThread()); } else { // If the type doesn't match the above, we assume we have a texture, and the type specified is the diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 639ab8b948..544786f03e 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -271,7 +271,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, // setup a baker for this material QSharedPointer materialBaker { - new MaterialBaker(data, isURL, _contentOutputPath), + new MaterialBaker(data, isURL, _contentOutputPath, _destinationPath), &MaterialBaker::deleteLater }; From c8209aa976860eae67f74cf08b7004f5a3229b02 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 14:31:14 -0700 Subject: [PATCH 068/117] Do not have multiple copies of the original texture file in the baked output --- libraries/baking/src/TextureBaker.cpp | 17 +++++++++-------- libraries/baking/src/TextureBaker.h | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 8591cbd0aa..dfc684ddee 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -47,17 +47,19 @@ TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type tex auto originalFilename = textureURL.fileName(); _baseFilename = originalFilename.left(originalFilename.lastIndexOf('.')); } + + _originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName()); } void TextureBaker::bake() { // once our texture is loaded, kick off a the processing connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); - if (_originalTexture.isEmpty()) { + if (_originalTexture.isEmpty() && !QFile(_originalCopyFilePath.toString()).exists()) { // first load the texture (either locally or remotely) loadTexture(); } else { - // we already have a texture passed to us, use that + // we already have a texture passed to us, or the texture is already saved, so use that emit originalTextureLoaded(); } } @@ -128,23 +130,22 @@ void TextureBaker::processTexture() { TextureMeta meta; - QString newFilename = _textureURL.fileName(); - QString addMapChannel = QString::fromStdString("_" + std::to_string(_textureType)); - newFilename.replace(QString("."), addMapChannel + "."); - QString originalCopyFilePath = _outputDirectory.absoluteFilePath(newFilename); + QString originalCopyFilePath = _originalCopyFilePath.toString(); + // Copy the original file into the baked output directory if it doesn't exist yet { QFile file { originalCopyFilePath }; - if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) { + if (!file.exists() && (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1)) { handleError("Could not write original texture for " + _textureURL.toString()); return; } // IMPORTANT: _originalTexture is empty past this point _originalTexture.clear(); _outputFiles.push_back(originalCopyFilePath); - meta.original = _metaTexturePathPrefix + newFilename; + meta.original = _metaTexturePathPrefix + _originalCopyFilePath.fileName(); } + // Load the copy of the original file from the baked output directory. New images will be created using the original as the source data. auto buffer = std::static_pointer_cast(std::make_shared(originalCopyFilePath)); if (!buffer->open(QIODevice::ReadOnly)) { handleError("Could not open original file at " + originalCopyFilePath); diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index 84e7c57aa1..9b86d875e9 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -73,6 +73,7 @@ private: QDir _outputDirectory; QString _metaTextureFileName; QString _metaTexturePathPrefix; + QUrl _originalCopyFilePath; std::atomic _abortProcessing { false }; From c54b23f6477c9a84f526832504324a60dcc5a053 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Thu, 14 Mar 2019 15:18:45 -0700 Subject: [PATCH 069/117] Make material baking output unique textures per usage like model baking does --- libraries/baking/src/MaterialBaker.cpp | 4 ++- libraries/baking/src/MaterialBaker.h | 2 ++ libraries/baking/src/ModelBaker.cpp | 24 +------------ libraries/baking/src/ModelBaker.h | 4 ++- .../baking/src/baking/TextureFileNamer.cpp | 34 +++++++++++++++++++ .../baking/src/baking/TextureFileNamer.h | 30 ++++++++++++++++ 6 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 libraries/baking/src/baking/TextureFileNamer.cpp create mode 100644 libraries/baking/src/baking/TextureFileNamer.h diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 2752890f55..9fc359fe9e 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -129,8 +129,10 @@ void MaterialBaker::processMaterial() { QPair textureKey(textureURL, it->second); if (!_textureBakers.contains(textureKey)) { + auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), it->second); + QSharedPointer textureBaker { - new TextureBaker(textureURL, it->second, _textureOutputDir), + new TextureBaker(textureURL, it->second, _textureOutputDir, "", baseTextureFileName), &TextureBaker::deleteLater }; textureBaker->setMapChannel(mapChannel); diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index 98f931b61c..41ce31380e 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -15,6 +15,7 @@ #include "Baker.h" #include "TextureBaker.h" +#include "baking/TextureFileNamer.h" #include @@ -60,6 +61,7 @@ private: QScriptEngine _scriptEngine; static std::function _getNextOvenWorkerThreadOperator; + TextureFileNamer _textureFileNamer; }; #endif // !hifi_MaterialBaker_h diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 77584beb1b..0a5341cce4 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -417,7 +417,7 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture // 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 - baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo, textureType); + baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); _remappedTexturePaths[urlToTexture] = baseTextureFileName; } @@ -628,28 +628,6 @@ void ModelBaker::checkIfTexturesFinished() { } } -QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) { - // If two textures have the same URL but are used differently, we need to process them separately - QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); - - QString baseTextureFileName{ textureFileInfo.completeBaseName() + addMapChannel }; - - // 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[baseTextureFileName]; - - 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 - baseTextureFileName += "-" + QString::number(nameMatches); - } - - // increment the number of name matches - ++nameMatches; - - return baseTextureFileName; -} - void ModelBaker::setWasAborted(bool wasAborted) { if (wasAborted != _wasAborted.load()) { Baker::setWasAborted(wasAborted); diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 6ee7511ce3..17af2604a2 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -19,6 +19,7 @@ #include "Baker.h" #include "TextureBaker.h" +#include "baking/TextureFileNamer.h" #include "ModelBakingLoggingCategory.h" @@ -97,7 +98,6 @@ private slots: void handleAbortedTexture(); private: - QString createBaseTextureFileName(const QFileInfo & textureFileInfo, const image::TextureUsage::Type textureType); QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); @@ -109,6 +109,8 @@ private: bool _pendingErrorEmission { false }; bool _hasBeenBaked { false }; + + TextureFileNamer _textureFileNamer; }; #endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/baking/TextureFileNamer.cpp b/libraries/baking/src/baking/TextureFileNamer.cpp new file mode 100644 index 0000000000..612d89e604 --- /dev/null +++ b/libraries/baking/src/baking/TextureFileNamer.cpp @@ -0,0 +1,34 @@ +// +// TextureFileNamer.cpp +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/14. +// Copyright 2019 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 "TextureFileNamer.h" + +QString TextureFileNamer::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) { + // If two textures have the same URL but are used differently, we need to process them separately + QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType)); + + QString baseTextureFileName{ textureFileInfo.baseName() + addMapChannel }; + + // 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[baseTextureFileName]; + + 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 + baseTextureFileName += "-" + QString::number(nameMatches); + } + + // increment the number of name matches + ++nameMatches; + + return baseTextureFileName; +} diff --git a/libraries/baking/src/baking/TextureFileNamer.h b/libraries/baking/src/baking/TextureFileNamer.h new file mode 100644 index 0000000000..9049588ef1 --- /dev/null +++ b/libraries/baking/src/baking/TextureFileNamer.h @@ -0,0 +1,30 @@ +// +// TextureFileNamer.h +// libraries/baking/src/baking +// +// Created by Sabrina Shanman on 2019/03/14. +// Copyright 2019 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_TextureFileNamer_h +#define hifi_TextureFileNamer_h + +#include +#include + +#include + +class TextureFileNamer { +public: + TextureFileNamer() {} + + QString createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType); + +protected: + QHash _textureNameMatchCount; +}; + +#endif // hifi_TextureFileNamer_h From d8b4419fd0d899ed48513f572f478be893ac37ee Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 14 Mar 2019 17:19:21 -0700 Subject: [PATCH 070/117] fix null object error on cancel export --- .../Editor/AvatarExporter/AvatarExporter.cs | 21 ++++++++++-------- .../avatarExporter.unitypackage | Bin 74600 -> 74623 bytes 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index f0d970031c..c5bc5eb84e 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -1531,16 +1531,19 @@ class ExportProjectWindow : EditorWindow { float GetAvatarHeight() { // height of an avatar model can be determined to be the max Y extents of the combined bounds for all its mesh renderers - Bounds bounds = new Bounds(); - var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); - var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); - foreach (var renderer in meshRenderers) { - bounds.Encapsulate(renderer.bounds); + if (avatarPreviewObject != null) { + Bounds bounds = new Bounds(); + var meshRenderers = avatarPreviewObject.GetComponentsInChildren(); + var skinnedMeshRenderers = avatarPreviewObject.GetComponentsInChildren(); + foreach (var renderer in meshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + foreach (var renderer in skinnedMeshRenderers) { + bounds.Encapsulate(renderer.bounds); + } + return bounds.max.y; } - foreach (var renderer in skinnedMeshRenderers) { - bounds.Encapsulate(renderer.bounds); - } - return bounds.max.y; + return 0.0f; } void SetAvatarScale(float actualScale) { diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 48a9502079839c3aee59ead39531198dd5d4bf86..3e2d6f2aed318abc6382f1a914ec3fb5e87dee69 100644 GIT binary patch delta 73664 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�(cEK1(J6z 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 K6EI-_Rs#UBWxK@y delta 73684 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%0DDvbGex2CKm{QT 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 Date: Fri, 15 Mar 2019 10:20:58 -0700 Subject: [PATCH 071/117] Attempt to fix build warnings --- libraries/baking/src/MaterialBaker.cpp | 21 +++++++++++---------- libraries/fbx/src/FBXSerializer_Mesh.cpp | 8 ++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 9fc359fe9e..b2392e0cb7 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -110,16 +110,17 @@ void MaterialBaker::processMaterial() { QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); // FIXME: this isn't properly handling bumpMaps or glossMaps - static const std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP({ - { graphics::Material::MapChannel::EMISSIVE_MAP, image::TextureUsage::EMISSIVE_TEXTURE }, - { graphics::Material::MapChannel::ALBEDO_MAP, image::TextureUsage::ALBEDO_TEXTURE }, - { graphics::Material::MapChannel::METALLIC_MAP, image::TextureUsage::METALLIC_TEXTURE }, - { graphics::Material::MapChannel::ROUGHNESS_MAP, image::TextureUsage::ROUGHNESS_TEXTURE }, - { graphics::Material::MapChannel::NORMAL_MAP, image::TextureUsage::NORMAL_TEXTURE }, - { graphics::Material::MapChannel::OCCLUSION_MAP, image::TextureUsage::OCCLUSION_TEXTURE }, - { graphics::Material::MapChannel::LIGHTMAP_MAP, image::TextureUsage::LIGHTMAP_TEXTURE }, - { graphics::Material::MapChannel::SCATTERING_MAP, image::TextureUsage::SCATTERING_TEXTURE } - }); + static std::unordered_map MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP; + if (MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.empty()) { + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::EMISSIVE_MAP] = image::TextureUsage::EMISSIVE_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ALBEDO_MAP] = image::TextureUsage::ALBEDO_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::METALLIC_MAP] = image::TextureUsage::METALLIC_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ROUGHNESS_MAP] = image::TextureUsage::ROUGHNESS_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::NORMAL_MAP] = image::TextureUsage::NORMAL_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::OCCLUSION_MAP] = image::TextureUsage::OCCLUSION_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::LIGHTMAP_MAP] = image::TextureUsage::LIGHTMAP_TEXTURE; + MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::SCATTERING_MAP] = image::TextureUsage::SCATTERING_TEXTURE; + } auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel); if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) { diff --git a/libraries/fbx/src/FBXSerializer_Mesh.cpp b/libraries/fbx/src/FBXSerializer_Mesh.cpp index 2f5286291c..c34b4678c7 100644 --- a/libraries/fbx/src/FBXSerializer_Mesh.cpp +++ b/libraries/fbx/src/FBXSerializer_Mesh.cpp @@ -13,12 +13,20 @@ #pragma warning( push ) #pragma warning( disable : 4267 ) #endif +// gcc and clang +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" +#endif #include #ifdef _WIN32 #pragma warning( pop ) #endif +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif #include #include From 5dab4c00106a244666a03d0c6e5cadadca5f3845 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 15 Mar 2019 10:47:48 -0700 Subject: [PATCH 072/117] remove white-space --- scripts/system/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 469f30cd23..7fdb863a83 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -257,7 +257,7 @@ notice.isFacingAvatar = false; notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); + notifications.push(notificationText); } else { notifications.push(Overlays.addOverlay("image3d", notice)); } From 8748a7561b4f9f8b235593b608783652a05e7470 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 12:48:32 -0700 Subject: [PATCH 073/117] fix lighting/color grading for everything --- .../src/RenderablePolyLineEntityItem.cpp | 7 +- .../src/RenderableShapeEntityItem.cpp | 10 +-- .../src/RenderableTextEntityItem.cpp | 19 +++- .../render-utils/src/DeferredBufferWrite.slh | 8 +- libraries/render-utils/src/GeometryCache.cpp | 86 +------------------ libraries/render-utils/src/GeometryCache.h | 20 ----- libraries/render-utils/src/TextRenderer3D.cpp | 4 +- libraries/render-utils/src/TextRenderer3D.h | 2 +- .../render-utils/src/forward_sdf_text3D.slf | 57 ++++++++++++ .../src/forward_simple_textured.slf | 15 ++-- .../forward_simple_textured_transparent.slf | 22 +++-- .../src/render-utils/forward_sdf_text3D.slp | 1 + libraries/render-utils/src/sdf_text3D.slf | 44 ++-------- libraries/render-utils/src/sdf_text3D.slh | 63 ++++++++++++++ libraries/render-utils/src/sdf_text3D.slv | 11 ++- .../src/sdf_text3D_transparent.slf | 43 ++-------- libraries/render-utils/src/simple.slv | 1 - .../src/simple_transparent_textured.slf | 39 ++++++--- libraries/render-utils/src/text/Font.cpp | 60 ++++++++----- libraries/render-utils/src/text/Font.h | 3 +- 20 files changed, 260 insertions(+), 255 deletions(-) create mode 100644 libraries/render-utils/src/forward_sdf_text3D.slf create mode 100644 libraries/render-utils/src/render-utils/forward_sdf_text3D.slp create mode 100644 libraries/render-utils/src/sdf_text3D.slh diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 7050393221..98f79780be 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -46,12 +46,7 @@ PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) void PolyLineEntityRenderer::buildPipeline() { // FIXME: opaque pipeline - gpu::ShaderPointer program; - if (DISABLE_DEFERRED) { - program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke_forward); - } else { - program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke); - } + gpu::ShaderPointer program = gpu::Shader::createProgram(DISABLE_DEFERRED ? shader::entities_renderer::program::paintStroke_forward : shader::entities_renderer::program::paintStroke); { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 20837070d8..0ba3adbe9b 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -277,16 +277,10 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { } else if (!useMaterialPipeline(materials)) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - render::ShapePipelinePointer pipeline; - if (_renderLayer == RenderLayer::WORLD && !DISABLE_DEFERRED) { - pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - } else { - pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); - } if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { - geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); + geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); } else { - geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); + geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, args->_shapePipeline); } } else { if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 107847826c..5cd0abae68 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -21,6 +21,8 @@ #include +#include "DeferredLightingEffect.h" + using namespace render; using namespace render::entities; @@ -162,7 +164,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { glm::vec4 backgroundColor; Transform modelTransform; glm::vec3 dimensions; - bool forwardRendered; + bool layered; withReadLock([&] { modelTransform = _renderTransform; dimensions = _dimensions; @@ -172,7 +174,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { textColor = EntityRenderer::calculatePulseColor(textColor, _pulseProperties, _created); backgroundColor = glm::vec4(_backgroundColor, fadeRatio * _backgroundAlpha); backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created); - forwardRendered = _renderLayer != RenderLayer::WORLD || DISABLE_DEFERRED; + layered = _renderLayer != RenderLayer::WORLD; }); // Render background @@ -184,6 +186,11 @@ void TextEntityRenderer::doRender(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch& batch = *args->_batch; + // FIXME: we need to find a better way of rendering text so we don't have to do this + if (layered) { + DependencyManager::get()->setupKeyLightBatch(args, batch); + } + auto transformToTopLeft = modelTransform; transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left @@ -192,7 +199,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { if (backgroundColor.a > 0.0f) { batch.setModelTransform(transformToTopLeft); auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(batch, false, backgroundColor.a < 1.0f, false, false, false, true, forwardRendered); + geometryCache->bindSimpleProgram(batch, false, backgroundColor.a < 1.0f, false, false, false, true, layered); geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID); } @@ -203,7 +210,11 @@ void TextEntityRenderer::doRender(RenderArgs* args) { batch.setModelTransform(transformToTopLeft); glm::vec2 bounds = glm::vec2(dimensions.x - (_leftMargin + _rightMargin), dimensions.y - (_topMargin + _bottomMargin)); - _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, forwardRendered); + _textRenderer->draw(batch, _leftMargin / scale, -_topMargin / scale, _text, textColor, bounds / scale, layered); + } + + if (layered) { + DependencyManager::get()->unsetKeyLightBatch(batch); } } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index ea32c5ecb3..66d0aa2ddb 100644 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -29,7 +29,7 @@ float evalOpaqueFinalAlpha(float alpha, float mapAlpha) { <@include LightingModel.slh@> void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 emissive, float occlusion, float scattering) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } @@ -42,7 +42,7 @@ void packDeferredFragment(vec3 normal, float alpha, vec3 albedo, float roughness } void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float roughness, float metallic, vec3 lightmap) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } @@ -54,7 +54,7 @@ void packDeferredFragmentLightmap(vec3 normal, float alpha, vec3 albedo, float r } void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { - if (alpha != 1.0) { + if (alpha < 1.0) { discard; } _fragColor0 = vec4(color, packUnlit()); @@ -64,7 +64,7 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, float roughness) { - if (alpha <= 0.0) { + if (alpha < 1.e-6) { discard; } _fragColor0 = vec4(albedo.rgb, alpha); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 0f400e00ee..c189798a42 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -722,8 +722,6 @@ gpu::ShaderPointer GeometryCache::_unlitFadeShader; render::ShapePipelinePointer GeometryCache::_simpleOpaquePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentPipeline; -render::ShapePipelinePointer GeometryCache::_forwardSimpleOpaquePipeline; -render::ShapePipelinePointer GeometryCache::_forwardSimpleTransparentPipeline; render::ShapePipelinePointer GeometryCache::_simpleOpaqueFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; @@ -803,8 +801,6 @@ void GeometryCache::initializeShapePipelines() { if (!_simpleOpaquePipeline) { _simpleOpaquePipeline = getShapePipeline(false, false, true, false); _simpleTransparentPipeline = getShapePipeline(false, true, true, false); - _forwardSimpleOpaquePipeline = getShapePipeline(false, false, true, false, false, true); - _forwardSimpleTransparentPipeline = getShapePipeline(false, true, true, false, false, true); _simpleOpaqueFadePipeline = getFadingShapePipeline(false, false, false, false, false); _simpleTransparentFadePipeline = getFadingShapePipeline(false, true, false, false, false); _simpleWirePipeline = getShapePipeline(false, false, true, true); @@ -836,14 +832,6 @@ render::ShapePipelinePointer GeometryCache::getFadingShapePipeline(bool textured ); } -render::ShapePipelinePointer GeometryCache::getOpaqueShapePipeline(bool isFading) { - return isFading ? _simpleOpaqueFadePipeline : _simpleOpaquePipeline; -} - -render::ShapePipelinePointer GeometryCache::getTransparentShapePipeline(bool isFading) { - return isFading ? _simpleTransparentFadePipeline : _simpleTransparentPipeline; -} - void GeometryCache::renderShape(gpu::Batch& batch, Shape shape) { batch.setInputFormat(getSolidStreamFormat()); _shapes[shape].draw(batch); @@ -2018,77 +2006,6 @@ void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm batch.draw(gpu::LINES, 2, 0); } - -void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, float glowIntensity, float glowWidth, int id) { - - // Disable glow lines on OSX -#ifndef Q_OS_WIN - glowIntensity = 0.0f; -#endif - - if (glowIntensity <= 0.0f) { - if (color.a >= 1.0f) { - bindSimpleProgram(batch, false, false, false, true, true); - } else { - bindSimpleProgram(batch, false, true, false, true, true); - } - renderLine(batch, p1, p2, color, id); - return; - } - - // Compile the shaders - static std::once_flag once; - std::call_once(once, [&] { - auto state = std::make_shared(); - auto program = gpu::Shader::createProgram(shader::render_utils::program::glowLine); - state->setCullMode(gpu::State::CULL_NONE); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - PrepareStencil::testMask(*state); - _glowLinePipeline = gpu::Pipeline::create(program, state); - }); - - batch.setPipeline(_glowLinePipeline); - - Vec3Pair key(p1, p2); - bool registered = (id != UNKNOWN_ID); - BatchItemDetails& details = _registeredLine3DVBOs[id]; - - // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed - if (registered && details.isCreated) { - Vec3Pair& lastKey = _lastRegisteredLine3D[id]; - if (lastKey != key) { - details.clear(); - _lastRegisteredLine3D[id] = key; - } - } - - const int NUM_VERTICES = 4; - if (!details.isCreated) { - details.isCreated = true; - details.uniformBuffer = std::make_shared(); - - struct LineData { - vec4 p1; - vec4 p2; - vec4 color; - float width; - }; - - LineData lineData { vec4(p1, 1.0f), vec4(p2, 1.0f), color, glowWidth }; - details.uniformBuffer->resize(sizeof(LineData)); - details.uniformBuffer->setSubData(0, lineData); - } - - // The shader requires no vertices, only uniforms. - batch.setUniformBuffer(0, details.uniformBuffer); - batch.draw(gpu::TRIANGLE_STRIP, NUM_VERTICES, 0); -} - void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) { static std::once_flag once; std::call_once(once, [&]() { @@ -2282,8 +2199,7 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp _unlitShader = _forwardUnlitShader; } else { _simpleShader = gpu::Shader::createProgram(simple_textured); - // Use the forward pipeline for both here, otherwise transparents will be unlit - _transparentShader = gpu::Shader::createProgram(forward_simple_textured_transparent); + _transparentShader = gpu::Shader::createProgram(simple_transparent_textured); _unlitShader = gpu::Shader::createProgram(simple_textured_unlit); } }); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 5c4cc67adf..cd3454bf38 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -181,17 +181,6 @@ public: static void initializeShapePipelines(); - render::ShapePipelinePointer getOpaqueShapePipeline() { assert(_simpleOpaquePipeline != nullptr); return _simpleOpaquePipeline; } - render::ShapePipelinePointer getTransparentShapePipeline() { assert(_simpleTransparentPipeline != nullptr); return _simpleTransparentPipeline; } - render::ShapePipelinePointer getForwardOpaqueShapePipeline() { assert(_forwardSimpleOpaquePipeline != nullptr); return _forwardSimpleOpaquePipeline; } - render::ShapePipelinePointer getForwardTransparentShapePipeline() { assert(_forwardSimpleTransparentPipeline != nullptr); return _forwardSimpleTransparentPipeline; } - render::ShapePipelinePointer getOpaqueFadeShapePipeline() { assert(_simpleOpaqueFadePipeline != nullptr); return _simpleOpaqueFadePipeline; } - render::ShapePipelinePointer getTransparentFadeShapePipeline() { assert(_simpleTransparentFadePipeline != nullptr); return _simpleTransparentFadePipeline; } - render::ShapePipelinePointer getOpaqueShapePipeline(bool isFading); - render::ShapePipelinePointer getTransparentShapePipeline(bool isFading); - render::ShapePipelinePointer getWireShapePipeline() { assert(_simpleWirePipeline != nullptr); return GeometryCache::_simpleWirePipeline; } - - // Static (instanced) geometry void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); @@ -317,12 +306,6 @@ public: void renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color1, const glm::vec4& color2, int id); - void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, - const glm::vec4& color, float glowIntensity, float glowWidth, int id); - - void renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2, const glm::vec4& color, int id) - { renderGlowLine(batch, p1, p2, color, 1.0f, 0.05f, id); } - void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, int id) { renderDashedLine(batch, start, end, color, 0.05f, 0.025f, id); } @@ -478,12 +461,9 @@ private: static gpu::ShaderPointer _unlitFadeShader; static render::ShapePipelinePointer _simpleOpaquePipeline; static render::ShapePipelinePointer _simpleTransparentPipeline; - static render::ShapePipelinePointer _forwardSimpleOpaquePipeline; - static render::ShapePipelinePointer _forwardSimpleTransparentPipeline; static render::ShapePipelinePointer _simpleOpaqueFadePipeline; static render::ShapePipelinePointer _simpleTransparentFadePipeline; static render::ShapePipelinePointer _simpleWirePipeline; - gpu::PipelinePointer _glowLinePipeline; static QHash _simplePrograms; diff --git a/libraries/render-utils/src/TextRenderer3D.cpp b/libraries/render-utils/src/TextRenderer3D.cpp index 93edc4217d..8ef0dc0d73 100644 --- a/libraries/render-utils/src/TextRenderer3D.cpp +++ b/libraries/render-utils/src/TextRenderer3D.cpp @@ -67,11 +67,11 @@ float TextRenderer3D::getFontSize() const { } void TextRenderer3D::draw(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4& color, - const glm::vec2& bounds, bool forwardRendered) { + const glm::vec2& bounds, bool layered) { // The font does all the OpenGL work if (_font) { _color = color; - _font->drawString(batch, _drawInfo, str, _color, _effectType, { x, y }, bounds, forwardRendered); + _font->drawString(batch, _drawInfo, str, _color, _effectType, { x, y }, bounds, layered); } } diff --git a/libraries/render-utils/src/TextRenderer3D.h b/libraries/render-utils/src/TextRenderer3D.h index b6475ab0ed..6c91411e1d 100644 --- a/libraries/render-utils/src/TextRenderer3D.h +++ b/libraries/render-utils/src/TextRenderer3D.h @@ -39,7 +39,7 @@ public: float getFontSize() const; // Pixel size void draw(gpu::Batch& batch, float x, float y, const QString& str, const glm::vec4& color = glm::vec4(1.0f), - const glm::vec2& bounds = glm::vec2(-1.0f), bool forwardRendered = false); + const glm::vec2& bounds = glm::vec2(-1.0f), bool layered = false); private: TextRenderer3D(const char* family, float pointSize, int weight = -1, bool italic = false, diff --git a/libraries/render-utils/src/forward_sdf_text3D.slf b/libraries/render-utils/src/forward_sdf_text3D.slf new file mode 100644 index 0000000000..09b10c0c42 --- /dev/null +++ b/libraries/render-utils/src/forward_sdf_text3D.slf @@ -0,0 +1,57 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// sdf_text3D_transparent.frag +// fragment shader +// +// Created by Bradley Austin Davis on 2015-02-04 +// Based on fragment shader code from +// https://github.com/paulhoux/Cinder-Samples/blob/master/TextRendering/include/text/Text.cpp +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +<@include DefaultMaterials.slh@> + +<@include ForwardGlobalLight.slh@> +<$declareEvalSkyboxGlobalColor()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +<@include render-utils/ShaderConstants.h@> + +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> + +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; +layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; +#define _texCoord0 _texCoord01.xy +#define _texCoord1 _texCoord01.zw + +layout(location=0) out vec4 _fragColor0; + +void main() { + float a = evalSDFSuperSampled(_texCoord0); + + float alpha = a * _color.a; + if (alpha <= 0.0) { + discard; + } + + TransformCamera cam = getTransformCamera(); + vec3 fragPosition = _positionES.xyz; + + _fragColor0 = vec4(evalSkyboxGlobalColor( + cam._viewInverse, + 1.0, + DEFAULT_OCCLUSION, + fragPosition, + normalize(_normalWS), + _color.rgb, + DEFAULT_FRESNEL, + DEFAULT_METALLIC, + DEFAULT_ROUGHNESS), + 1.0); +} \ No newline at end of file diff --git a/libraries/render-utils/src/forward_simple_textured.slf b/libraries/render-utils/src/forward_simple_textured.slf index ca31550b40..373ab13d1a 100644 --- a/libraries/render-utils/src/forward_simple_textured.slf +++ b/libraries/render-utils/src/forward_simple_textured.slf @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include gpu/Color.slh@> <@include DefaultMaterials.slh@> <@include ForwardGlobalLight.slh@> @@ -21,10 +22,8 @@ <@include render-utils/ShaderConstants.h@> -// the albedo texture LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; @@ -36,7 +35,11 @@ layout(location=0) out vec4 _fragColor0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0); - float colorAlpha = _color.a * texel.a; + texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); + vec3 albedo = _color.xyz * texel.xyz; + float metallic = DEFAULT_METALLIC; + + vec3 fresnel = getFresnelF0(metallic, albedo); TransformCamera cam = getTransformCamera(); vec3 fragPosition = _positionES.xyz; @@ -47,9 +50,9 @@ void main(void) { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - _color.rgb * texel.rgb, - DEFAULT_FRESNEL, - DEFAULT_METALLIC, + albedo, + fresnel, + metallic, DEFAULT_ROUGHNESS), 1.0); } \ No newline at end of file diff --git a/libraries/render-utils/src/forward_simple_textured_transparent.slf b/libraries/render-utils/src/forward_simple_textured_transparent.slf index 11d51bbd78..1b5047507b 100644 --- a/libraries/render-utils/src/forward_simple_textured_transparent.slf +++ b/libraries/render-utils/src/forward_simple_textured_transparent.slf @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include gpu/Color.slh@> <@include DefaultMaterials.slh@> <@include ForwardGlobalLight.slh@> @@ -21,22 +22,25 @@ <@include render-utils/ShaderConstants.h@> -// the albedo texture LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw -layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=0) out vec4 _fragColor0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0); - float colorAlpha = _color.a * texel.a; + texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); + vec3 albedo = _color.xyz * texel.xyz; + float alpha = _color.a * texel.a; + float metallic = DEFAULT_METALLIC; + + vec3 fresnel = getFresnelF0(metallic, albedo); TransformCamera cam = getTransformCamera(); vec3 fragPosition = _positionES.xyz; @@ -47,10 +51,10 @@ void main(void) { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - _color.rgb * texel.rgb, - DEFAULT_FRESNEL, - DEFAULT_METALLIC, + albedo, + fresnel, + metallic, DEFAULT_EMISSIVE, - DEFAULT_ROUGHNESS, colorAlpha), - colorAlpha); + DEFAULT_ROUGHNESS, alpha), + alpha); } \ No newline at end of file diff --git a/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp b/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp new file mode 100644 index 0000000000..3eea3a0da0 --- /dev/null +++ b/libraries/render-utils/src/render-utils/forward_sdf_text3D.slp @@ -0,0 +1 @@ +VERTEX sdf_text3D diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf index b070fc44cf..91c73e9eec 100644 --- a/libraries/render-utils/src/sdf_text3D.slf +++ b/libraries/render-utils/src/sdf_text3D.slf @@ -13,54 +13,22 @@ <@include DeferredBufferWrite.slh@> <@include render-utils/ShaderConstants.h@> -LAYOUT(binding=0) uniform sampler2D Font; +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> -struct TextParams { - vec4 color; - vec4 outline; -}; - -LAYOUT(binding=0) uniform textParamsBuffer { - TextParams params; -}; - -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw -#define TAA_TEXTURE_LOD_BIAS -3.0 - -const float interiorCutoff = 0.8; -const float outlineExpansion = 0.2; -const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); - -float evalSDF(vec2 texCoord) { - // retrieve signed distance - float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; - sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); - - // Rely on TAA for anti-aliasing - return step(0.5, sdf); -} - void main() { - vec2 dxTexCoord = dFdx(_texCoord0) * 0.5 * taaBias; - vec2 dyTexCoord = dFdy(_texCoord0) * 0.5 * taaBias; - - // Perform 4x supersampling for anisotropic filtering - float a; - a = evalSDF(_texCoord0); - a += evalSDF(_texCoord0 + dxTexCoord); - a += evalSDF(_texCoord0 + dyTexCoord); - a += evalSDF(_texCoord0 + dxTexCoord + dyTexCoord); - a *= 0.25; + float a = evalSDFSuperSampled(_texCoord0); packDeferredFragment( normalize(_normalWS), - a * params.color.a, - params.color.rgb, + a, + _color.rgb, DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, diff --git a/libraries/render-utils/src/sdf_text3D.slh b/libraries/render-utils/src/sdf_text3D.slh new file mode 100644 index 0000000000..3297596efd --- /dev/null +++ b/libraries/render-utils/src/sdf_text3D.slh @@ -0,0 +1,63 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> + +// Generated on <$_SCRIBE_DATE$> +// +// Created by Sam Gondelman on 3/15/19 +// Copyright 2019 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 +// +!> +<@if not SDF_TEXT3D_SLH@> +<@def SDF_TEXT3D_SLH@> + +LAYOUT(binding=0) uniform sampler2D Font; + +struct TextParams { + vec4 color; + vec4 outline; +}; + +LAYOUT(binding=0) uniform textParamsBuffer { + TextParams params; +}; + +<@func declareEvalSDFSuperSampled()@> + +#define TAA_TEXTURE_LOD_BIAS -3.0 + +const float interiorCutoff = 0.8; +const float outlineExpansion = 0.2; +const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); + +float evalSDF(vec2 texCoord) { + // retrieve signed distance + float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; + sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); + + // Rely on TAA for anti-aliasing + return step(0.5, sdf); +} + +float evalSDFSuperSampled(vec2 texCoord) { + vec2 dxTexCoord = dFdx(texCoord) * 0.5 * taaBias; + vec2 dyTexCoord = dFdy(texCoord) * 0.5 * taaBias; + + // Perform 4x supersampling for anisotropic filtering + float a; + a = evalSDF(texCoord); + a += evalSDF(texCoord + dxTexCoord); + a += evalSDF(texCoord + dyTexCoord); + a += evalSDF(texCoord + dxTexCoord + dyTexCoord); + a *= 0.25; + + return a; +} + +<@endfunc@> + +<@endif@> + diff --git a/libraries/render-utils/src/sdf_text3D.slv b/libraries/render-utils/src/sdf_text3D.slv index 5f4df86d56..274e09e6ad 100644 --- a/libraries/render-utils/src/sdf_text3D.slv +++ b/libraries/render-utils/src/sdf_text3D.slv @@ -11,18 +11,23 @@ // <@include gpu/Inputs.slh@> -<@include gpu/Transform.slh@> +<@include gpu/Color.slh@> <@include render-utils/ShaderConstants.h@> +<@include gpu/Transform.slh@> <$declareStandardTransform()$> +<@include sdf_text3D.slh@> + // the interpolated normal -layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; -layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; layout(location=RENDER_UTILS_ATTR_POSITION_ES) out vec4 _positionES; +layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; void main() { _texCoord01.xy = inTexCoord0.xy; + _color = color_sRGBAToLinear(params.color); // standard transform TransformCamera cam = getTransformCamera(); diff --git a/libraries/render-utils/src/sdf_text3D_transparent.slf b/libraries/render-utils/src/sdf_text3D_transparent.slf index 311c849915..c4a80091de 100644 --- a/libraries/render-utils/src/sdf_text3D_transparent.slf +++ b/libraries/render-utils/src/sdf_text3D_transparent.slf @@ -20,53 +20,22 @@ <@include render-utils/ShaderConstants.h@> -LAYOUT(binding=0) uniform sampler2D Font; - -struct TextParams { - vec4 color; - vec4 outline; -}; - -LAYOUT(binding=0) uniform textParamsBuffer { - TextParams params; -}; +<@include sdf_text3D.slh@> +<$declareEvalSDFSuperSampled()$> layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; +layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw layout(location=0) out vec4 _fragColor0; -#define TAA_TEXTURE_LOD_BIAS -3.0 - -const float interiorCutoff = 0.8; -const float outlineExpansion = 0.2; -const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS); - -float evalSDF(vec2 texCoord) { - // retrieve signed distance - float sdf = textureLod(Font, texCoord, TAA_TEXTURE_LOD_BIAS).g; - sdf = mix(sdf, mix(sdf + outlineExpansion, 1.0 - sdf, float(sdf > interiorCutoff)), float(params.outline.x > 0.0)); - - // Rely on TAA for anti-aliasing - return step(0.5, sdf); -} - void main() { - vec2 dxTexCoord = dFdx(_texCoord0) * 0.5 * taaBias; - vec2 dyTexCoord = dFdy(_texCoord0) * 0.5 * taaBias; + float a = evalSDFSuperSampled(_texCoord0); - // Perform 4x supersampling for anisotropic filtering - float a; - a = evalSDF(_texCoord0); - a += evalSDF(_texCoord0 + dxTexCoord); - a += evalSDF(_texCoord0 + dyTexCoord); - a += evalSDF(_texCoord0 + dxTexCoord + dyTexCoord); - a *= 0.25; - - float alpha = a * params.color.a; + float alpha = a * _color.a; if (alpha <= 0.0) { discard; } @@ -80,7 +49,7 @@ void main() { DEFAULT_OCCLUSION, fragPosition, normalize(_normalWS), - params.color.rgb, + _color.rgb, DEFAULT_FRESNEL, DEFAULT_METALLIC, DEFAULT_EMISSIVE, diff --git a/libraries/render-utils/src/simple.slv b/libraries/render-utils/src/simple.slv index 0dd4e55f26..460ed53281 100644 --- a/libraries/render-utils/src/simple.slv +++ b/libraries/render-utils/src/simple.slv @@ -19,7 +19,6 @@ <@include render-utils/ShaderConstants.h@> -// the interpolated normal layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_NORMAL_MS) out vec3 _normalMS; layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color; diff --git a/libraries/render-utils/src/simple_transparent_textured.slf b/libraries/render-utils/src/simple_transparent_textured.slf index bd29ff2ec9..f1bb2b1ea2 100644 --- a/libraries/render-utils/src/simple_transparent_textured.slf +++ b/libraries/render-utils/src/simple_transparent_textured.slf @@ -11,31 +11,50 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include DefaultMaterials.slh@> <@include gpu/Color.slh@> -<@include DeferredBufferWrite.slh@> - <@include render-utils/ShaderConstants.h@> -// the albedo texture +<@include ForwardGlobalLight.slh@> +<$declareEvalGlobalLightingAlphaBlended()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + LAYOUT(binding=0) uniform sampler2D originalTexture; -// the interpolated normal +layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES; layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS; layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color; layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord0 _texCoord01.xy #define _texCoord1 _texCoord01.zw +layout(location=0) out vec4 _fragColor0; + void main(void) { vec4 texel = texture(originalTexture, _texCoord0); texel = mix(texel, color_sRGBAToLinear(texel), float(_color.a <= 0.0)); - texel.rgb *= _color.rgb; - texel.a *= abs(_color.a); + vec3 albedo = _color.xyz * texel.xyz; + float alpha = _color.a * texel.a; + float metallic = DEFAULT_METALLIC; - packDeferredFragmentTranslucent( + vec3 fresnel = getFresnelF0(metallic, albedo); + + TransformCamera cam = getTransformCamera(); + vec3 fragPosition = _positionES.xyz; + + _fragColor0 = vec4(evalGlobalLightingAlphaBlendedWithHaze( + cam._viewInverse, + 1.0, + DEFAULT_OCCLUSION, + fragPosition, normalize(_normalWS), - texel.a, - texel.rgb, - DEFAULT_ROUGHNESS); + albedo, + fresnel, + metallic, + DEFAULT_EMISSIVE, + DEFAULT_ROUGHNESS, alpha), + alpha); } \ No newline at end of file diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index e0e99da020..364e24c5ac 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -13,6 +13,8 @@ #include "FontFamilies.h" #include "../StencilMaskPass.h" +#include "DisableDeferred.h" + static std::mutex fontMutex; struct TextureVertex { @@ -221,25 +223,43 @@ void Font::setupGPU() { // Setup render pipeline { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D); - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMaskDrawShape(*state); - _pipeline = gpu::Pipeline::create(program, state); + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::forward_sdf_text3D); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShape(*state); + _layeredPipeline = gpu::Pipeline::create(program, state); + } - gpu::ShaderPointer programTransparent = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D_transparent); - auto transparentState = std::make_shared(); - transparentState->setCullMode(gpu::State::CULL_BACK); - transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); - transparentState->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*transparentState); - _transparentPipeline = gpu::Pipeline::create(programTransparent, transparentState); + if (DISABLE_DEFERRED) { + _pipeline = _layeredPipeline; + } else { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMaskDrawShape(*state); + _pipeline = gpu::Pipeline::create(program, state); + } + + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::render_utils::program::sdf_text3D_transparent); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); + _transparentPipeline = gpu::Pipeline::create(program, state); + } } // Sanity checks @@ -343,7 +363,7 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm } void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const QString& str, const glm::vec4& color, - EffectType effectType, const glm::vec2& origin, const glm::vec2& bounds, bool forwardRendered) { + EffectType effectType, const glm::vec2& origin, const glm::vec2& bounds, bool layered) { if (str == "") { return; } @@ -370,7 +390,7 @@ void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const QString } // need the gamma corrected color here - batch.setPipeline(forwardRendered || (color.a < 1.0f) ? _transparentPipeline : _pipeline); + batch.setPipeline(color.a < 1.0f ? _transparentPipeline : (layered ? _layeredPipeline : _pipeline)); batch.setInputFormat(_format); batch.setInputBuffer(0, drawInfo.verticesBuffer, 0, _format->getChannels().at(0)._stride); batch.setResourceTexture(render_utils::slot::texture::TextFont, _texture); diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h index 26cc4e46c3..28af5bac43 100644 --- a/libraries/render-utils/src/text/Font.h +++ b/libraries/render-utils/src/text/Font.h @@ -46,7 +46,7 @@ public: // Render string to batch void drawString(gpu::Batch& batch, DrawInfo& drawInfo, const QString& str, const glm::vec4& color, EffectType effectType, - const glm::vec2& origin, const glm::vec2& bound, bool forwardRendered); + const glm::vec2& origin, const glm::vec2& bound, bool layered); static Pointer load(const QString& family); @@ -81,6 +81,7 @@ private: // gpu structures gpu::PipelinePointer _pipeline; + gpu::PipelinePointer _layeredPipeline; gpu::PipelinePointer _transparentPipeline; gpu::TexturePointer _texture; gpu::Stream::FormatPointer _format; From 9182db8bd40b0accc57aa65f963cc7a3efdf981f Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 15 Mar 2019 12:56:43 -0700 Subject: [PATCH 074/117] Case 21025 conditionalizing TabletWebView features --- interface/resources/qml/hifi/Card.qml | 5 ++-- interface/resources/qml/hifi/NameCard.qml | 24 ++++++++++++------- .../qml/hifi/commerce/wallet/Wallet.qml | 7 ++++-- .../qml/hifi/commerce/wallet/WalletHome.qml | 6 ++++- .../qml/hifi/tablet/TabletAddressDialog.qml | 7 ++++-- interface/src/Application.cpp | 3 +++ 6 files changed, 36 insertions(+), 16 deletions(-) diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index fc49bcf048..9fb8067371 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -40,6 +40,7 @@ Item { property bool isConcurrency: action === 'concurrency'; property bool isAnnouncement: action === 'announcement'; property bool isStacked: !isConcurrency && drillDownToPlace; + property bool has3DHTML: PlatformInfo.has3DHTML(); property int textPadding: 10; @@ -298,7 +299,7 @@ Item { StateImage { id: actionIcon; - visible: !isAnnouncement; + visible: !isAnnouncement && has3DHTML; imageURL: "../../images/info-icon-2-state.svg"; size: 30; buttonState: messageArea.containsMouse ? 1 : 0; @@ -315,7 +316,7 @@ Item { } MouseArea { id: messageArea; - visible: !isAnnouncement; + visible: !isAnnouncement && has3DHTML; width: parent.width; height: messageHeight; anchors.top: lobby.bottom; diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 646fc881e1..c92afe9e14 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -46,6 +46,8 @@ Item { property string placeName: "" property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent")) property alias avImage: avatarImage + property bool has3DHTML: PlatformInfo.has3DHTML(); + Item { id: avatarImage visible: profileUrl !== "" && userName !== ""; @@ -94,10 +96,12 @@ Item { enabled: (selected && activeTab == "nearbyTab") || isMyCard; hoverEnabled: enabled onClicked: { - userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; - userInfoViewer.visible = true; + if (Phas3DHTML) { + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; + userInfoViewer.visible = true; + } } - onEntered: infoHoverImage.visible = true; + onEntered: infoHoverImage.visible = has3DHTML; onExited: infoHoverImage.visible = false; } } @@ -352,7 +356,7 @@ Item { } StateImage { id: nameCardConnectionInfoImage - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && has3DHTML imageURL: "../../images/info-icon-2-state.svg" // PLACEHOLDER!!! size: 32; buttonState: 0; @@ -364,8 +368,10 @@ Item { enabled: selected hoverEnabled: true onClicked: { - userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; - userInfoViewer.visible = true; + if(has3DHTML) { + userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; + userInfoViewer.visible = true; + } } onEntered: { nameCardConnectionInfoImage.buttonState = 1; @@ -376,8 +382,7 @@ Item { } FiraSansRegular { id: nameCardConnectionInfoText - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" - width: parent.width + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && PlatformInfo.has3DHTML() height: displayNameTextPixelSize size: displayNameTextPixelSize - 4 anchors.left: nameCardConnectionInfoImage.right @@ -391,9 +396,10 @@ Item { id: nameCardRemoveConnectionImage visible: selected && !isMyCard && pal.activeTab == "connectionsTab" text: hifi.glyphs.close - size: 28; + size: 24; x: 120 anchors.verticalCenter: nameCardConnectionInfoImage.verticalCenter + anchors.left: has3DHTML ? nameCardConnectionInfoText.right + 10 : avatarImage.right } MouseArea { anchors.fill:nameCardRemoveConnectionImage diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ea74549084..7c2b86ef99 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -32,6 +32,7 @@ Rectangle { property string initialActiveViewAfterStatus5: "walletInventory"; property bool keyboardRaised: false; property bool isPassword: false; + property bool has3DHTML: PlatformInfo.has3DHTML(); anchors.fill: (typeof parent === undefined) ? undefined : parent; @@ -335,8 +336,10 @@ Rectangle { Connections { onSendSignalToWallet: { if (msg.method === 'transactionHistory_usernameLinkClicked') { - userInfoViewer.url = msg.usernameLink; - userInfoViewer.visible = true; + if (has3DHTML) { + userInfoViewer.url = msg.usernameLink; + userInfoViewer.visible = true; + } } else { sendToScript(msg); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index eb8aa0f809..06d07a28c9 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -24,6 +24,8 @@ Item { HifiConstants { id: hifi; } id: root; + + property bool has3DHTML: PlatformInfo.has3DHTML(); onVisibleChanged: { if (visible) { @@ -333,7 +335,9 @@ Item { onLinkActivated: { if (link.indexOf("users/") !== -1) { - sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); + if (has3DHTML) { + sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); + } } else { sendSignalToWallet({method: 'transactionHistory_linkClicked', itemId: model.marketplace_item}); } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 4edae017d1..1342e55b5d 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -35,6 +35,7 @@ StackView { property int cardWidth: 212; property int cardHeight: 152; property var tablet: null; + property bool has3DHTML: PlatformInfo.has3DHTML(); RootHttpRequest { id: http; } signal sendToScript(var message); @@ -75,8 +76,10 @@ StackView { } function goCard(targetString, standaloneOptimized) { if (0 !== targetString.indexOf('hifi://')) { - var card = tabletWebView.createObject(); - card.url = addressBarDialog.metaverseServerUrl + targetString; + if(has3DHTML) { + var card = tabletWebView.createObject(); + card.url = addressBarDialog.metaverseServerUrl + targetString; + } card.parentStackItem = root; root.push(card); return; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..417c44fc1f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3043,6 +3043,9 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/wallet/Wallet.qml" }, QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, QUrl{ "hifi/tablet/TabletAddressDialog.qml" }, + QUrl{ "hifi/Card.qml" }, + QUrl{ "hifi/Pal.qml" }, + QUrl{ "hifi/NameCard.qml" }, }, platformInfoCallback); QmlContextCallback ttsCallback = [](QQmlContext* context) { From 83bac723ef10a0c35024c9efb6fc493fafc5ac1b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 14 Mar 2019 10:59:42 -0700 Subject: [PATCH 075/117] fix wearable duplication on domain switch --- interface/src/Application.cpp | 4 +-- interface/src/avatar/MyAvatar.cpp | 28 +++++++++++++++++++ interface/src/avatar/MyAvatar.h | 6 ++++ .../entities/src/EntityScriptingInterface.cpp | 12 ++------ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..879426ec96 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5772,6 +5772,7 @@ void Application::reloadResourceCaches() { queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + getMyAvatar()->prepareAvatarEntityDataForReload(); // Clear the entities and their renderables getEntities()->clear(); @@ -6947,9 +6948,6 @@ void Application::updateWindowTitle() const { } void Application::clearDomainOctreeDetails(bool clearAll) { - // before we delete all entities get MyAvatar's AvatarEntityData ready - getMyAvatar()->prepareAvatarEntityDataForReload(); - // if we're about to quit, we really don't need to do the rest of these things... if (_aboutToQuit) { return; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9211be3b4f..02ef91cdba 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3450,6 +3450,34 @@ float MyAvatar::getGravity() { return _characterController.getGravity(); } +void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { + QUuid oldID = getSessionUUID(); + Avatar::setSessionUUID(sessionUUID); + QUuid id = getSessionUUID(); + if (id != oldID) { + auto treeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + entityTree->withWriteLock([&] { + for (const auto& entityID : avatarEntityIDs) { + auto entity = entityTree->findEntityByID(entityID); + if (!entity) { + continue; + } + entity->setOwningAvatarID(id); + if (entity->getParentID() == oldID) { + entity->setParentID(id); + } + } + }); + } + } +} + void MyAvatar::increaseSize() { float minScale = getDomainMinScale(); float maxScale = getDomainMaxScale(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index e516364f61..aadc8ee268 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1213,6 +1213,12 @@ public: public slots: + /**jsdoc + * @function MyAvatar.setSessionUUID + * @param {Uuid} sessionUUID + */ + virtual void setSessionUUID(const QUuid& sessionUUID) override; + /**jsdoc * Increase the avatar's scale by five percent, up to a minimum scale of 1000. * @function MyAvatar.increaseSize diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 22cd26eac6..6610439183 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1646,11 +1646,9 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); - EntityItemProperties properties; - EntityItemPointer entity; bool doTransmit = false; - _entityTree->withWriteLock([this, &entity, entityID, myNodeID, &doTransmit, actor, &properties] { + _entityTree->withWriteLock([this, &entity, entityID, myNodeID, &doTransmit, actor] { EntitySimulationPointer simulation = _entityTree->getSimulation(); entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { @@ -1669,16 +1667,12 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID, doTransmit = actor(simulation, entity); _entityTree->entityChanged(entity); - if (doTransmit) { - properties.setEntityHostType(entity->getEntityHostType()); - properties.setOwningAvatarID(entity->getOwningAvatarID()); - } }); // transmit the change if (doTransmit) { - _entityTree->withReadLock([&] { - properties = entity->getProperties(); + EntityItemProperties properties = _entityTree->resultWithReadLock([&] { + return entity->getProperties(); }); properties.setActionDataDirty(); From e8cac1f5985453a704b6c8dd2a687e880a9907f0 Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 15 Mar 2019 14:14:16 -0700 Subject: [PATCH 076/117] fix 2017 Unity versions --- .../Editor/AvatarExporter/AvatarExporter.cs | 11 ++++++----- tools/unity-avatar-exporter/Assets/README.txt | 2 +- .../avatarExporter.unitypackage | Bin 74623 -> 74591 bytes 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index c5bc5eb84e..87f401d478 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,7 +17,7 @@ using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.5"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.6"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -56,6 +56,8 @@ class AvatarExporter : MonoBehaviour { "2018.1.0f2", "2017.4.18f1", "2017.4.17f1", + "2017.4.16f1", + "2017.4.15f1", }; static readonly Dictionary HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary { @@ -1159,8 +1161,7 @@ class AvatarExporter : MonoBehaviour { static string GetMaterialTexture(Material material, string textureProperty) { // ensure the texture property name exists in this material and return its texture directory path if so - string[] textureNames = material.GetTexturePropertyNames(); - if (Array.IndexOf(textureNames, textureProperty) >= 0) { + if (material.HasProperty(textureProperty)) { Texture texture = material.GetTexture(textureProperty); if (texture) { foreach (var textureDependency in textureDependencies) { @@ -1214,10 +1215,10 @@ class AvatarExporter : MonoBehaviour { } if (unsupportedShaderMaterials.Count > 1) { warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " + - "Please change them to a Standard shader type.\n\n"; + "We recommend you change them to a Standard shader type.\n\n"; } else if (unsupportedShaderMaterials.Count == 1) { warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " + - "Please change it to a Standard shader type.\n\n"; + "We recommend you change it to a Standard shader type.\n\n"; } } diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 767c093800..5b228ebf75 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.5 +Version 0.3.6 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 3e2d6f2aed318abc6382f1a914ec3fb5e87dee69..05ad49baa6bfa1696cb12f659750246f2a6d2d9e 100644 GIT binary patch literal 74591 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

Ly1hzSwUsq8RmDStkK6pT6F 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

=)-SS*oWeSIvIm~V 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ફV}v>+vQ2ynK6RmQ**XvRgr;$h1}&DMk2nm!lYX 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 B?KS`a From 5f3e31b119bd5ad6feda0fa6590254b1a090def2 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 15 Mar 2019 18:15:18 -0700 Subject: [PATCH 084/117] add ui to kick api --- .../qml/dialogs/TabletMessageBox.qml | 2 +- .../resources/qml/hifi/tablet/TabletRoot.qml | 1 - libraries/script-engine/CMakeLists.txt | 2 +- .../src/UsersScriptingInterface.cpp | 41 ++++++++++++++++++- .../src/UsersScriptingInterface.h | 4 ++ libraries/ui/src/OffscreenUi.cpp | 6 +++ libraries/ui/src/OffscreenUi.h | 5 ++- .../ui/src/ui/TabletScriptingInterface.cpp | 1 + 8 files changed, 56 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/dialogs/TabletMessageBox.qml b/interface/resources/qml/dialogs/TabletMessageBox.qml index 1e6f0734ad..4411651a0f 100644 --- a/interface/resources/qml/dialogs/TabletMessageBox.qml +++ b/interface/resources/qml/dialogs/TabletMessageBox.qml @@ -28,7 +28,7 @@ TabletModalWindow { id: mouse; anchors.fill: parent } - + function click(button) { clickedButton = button; selected(button); diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 8d237d146a..5559c36fd1 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -117,7 +117,6 @@ Rectangle { if (loader.item.hasOwnProperty("gotoPreviousApp")) { loader.item.gotoPreviousApp = true; } - screenChanged("Web", url) }); } diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 82c408f386..e3eb8684d1 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -17,6 +17,6 @@ if (NOT ANDROID) endif () -link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi) +link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi ui qml) # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit include_hifi_library_headers(gl) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index fef11c12e9..631f0eb743 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -12,6 +12,8 @@ #include "UsersScriptingInterface.h" #include +#include +#include UsersScriptingInterface::UsersScriptingInterface() { // emit a signal when kick permissions have changed @@ -52,8 +54,43 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { } void UsersScriptingInterface::kick(const QUuid& nodeID) { - // ask the NodeList to kick the user with the given session ID - DependencyManager::get()->kickNodeBySessionID(nodeID); + bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); + if (getCanKick() && !waitingForKickResponse) { + + + auto avatarHashMap = DependencyManager::get(); + auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); + + QString userName; + + if (avatar) { + userName = avatar->getSessionDisplayName(); + } else { + userName = nodeID.toString(); + } + + QString kickMessage = "Do you wish to kick " + userName + " from your domain"; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage, + QMessageBox::Yes | QMessageBox::No); + + if (dlg->getDialogItem()) { + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool yes = (static_cast(answer.toInt()) == QMessageBox::Yes); + // ask the NodeList to kick the user with the given session ID + + if (yes) { + DependencyManager::get()->kickNodeBySessionID(nodeID); + } + + _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = false; }); + }); + + _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = true; }); + } + } } void UsersScriptingInterface::mute(const QUuid& nodeID) { diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 57de205066..0e3f9be0e0 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -15,6 +15,7 @@ #define hifi_UsersScriptingInterface_h #include +#include /**jsdoc * @namespace Users @@ -195,6 +196,9 @@ signals: private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); + + ReadWriteLockable _kickResponseLock; + bool _waitingForKickResponse { false }; }; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 137cffde94..2f2d38fe2a 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -240,6 +240,12 @@ class MessageBoxListener : public ModalDialogListener { return static_cast(_result.toInt()); } +protected slots: + virtual void onDestroyed() override { + ModalDialogListener::onDestroyed(); + onSelected(QMessageBox::NoButton); + } + private slots: void onSelected(int button) { _result = button; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 46dbdbdf13..6abbc486d0 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -34,6 +34,9 @@ class ModalDialogListener : public QObject { Q_OBJECT friend class OffscreenUi; +public: + QQuickItem* getDialogItem() { return _dialog; }; + protected: ModalDialogListener(QQuickItem* dialog); virtual ~ModalDialogListener(); @@ -43,7 +46,7 @@ signals: void response(const QVariant& value); protected slots: - void onDestroyed(); + virtual void onDestroyed(); protected: QQuickItem* _dialog; diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 7a1c37af33..bddb306dca 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -368,6 +368,7 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { if (toolbarMode) { #if !defined(DISABLE_QML) + closeDialog(); // create new desktop window auto tabletRootWindow = new TabletRootWindow(); tabletRootWindow->initQml(QVariantMap()); From 2ab8eb98e8ee5c945f8a98b03fc7aa17f28dda77 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Sun, 17 Mar 2019 14:00:41 -0700 Subject: [PATCH 085/117] better implementation --- interface/src/Application.cpp | 36 ++++++++++++++++ interface/src/Application.h | 1 + libraries/script-engine/CMakeLists.txt | 2 +- .../src/UsersScriptingInterface.cpp | 42 +++---------------- .../src/UsersScriptingInterface.h | 8 ++++ 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..581b260751 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2342,6 +2342,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return viewFrustum.getPosition(); }); + DependencyManager::get()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); }); + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { bool isTablet = url == TabletScriptingInterface::QML; if (htmlContent) { @@ -3287,6 +3289,40 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { #endif } +void Application::userKickConfirmation(const QUuid& nodeID) { + auto avatarHashMap = DependencyManager::get(); + auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); + + QString userName; + + if (avatar) { + userName = avatar->getSessionDisplayName(); + } else { + userName = nodeID.toString(); + } + + QString kickMessage = "Do you wish to kick " + userName + " from your domain"; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage, + QMessageBox::Yes | QMessageBox::No); + + if (dlg->getDialogItem()) { + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool yes = (static_cast(answer.toInt()) == QMessageBox::Yes); + // ask the NodeList to kick the user with the given session ID + + if (yes) { + DependencyManager::get()->kickNodeBySessionID(nodeID); + } + + DependencyManager::get()->setWaitForKickResponse(false); + }); + DependencyManager::get()->setWaitForKickResponse(true); + } +} + void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { surfaceContext->setContextProperty("Users", DependencyManager::get().data()); surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index a8cc9450c5..762ac9585a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -593,6 +593,7 @@ private: void toggleTabletUI(bool shouldOpen = false) const; static void setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties); + void userKickConfirmation(const QUuid& nodeID); MainWindow* _window; QElapsedTimer& _sessionRunTimer; diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index e3eb8684d1..82c408f386 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -17,6 +17,6 @@ if (NOT ANDROID) endif () -link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi ui qml) +link_hifi_libraries(shared networking octree shaders gpu procedural graphics material-networking model-networking ktx recording avatars fbx hfm entities controllers animation audio physics image midi) # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit include_hifi_library_headers(gl) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index 631f0eb743..9beb52f20a 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -12,8 +12,6 @@ #include "UsersScriptingInterface.h" #include -#include -#include UsersScriptingInterface::UsersScriptingInterface() { // emit a signal when kick permissions have changed @@ -54,42 +52,14 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) { } void UsersScriptingInterface::kick(const QUuid& nodeID) { - bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); - if (getCanKick() && !waitingForKickResponse) { - - auto avatarHashMap = DependencyManager::get(); - auto avatar = avatarHashMap->getAvatarBySessionID(nodeID); - - QString userName; - - if (avatar) { - userName = avatar->getSessionDisplayName(); - } else { - userName = nodeID.toString(); - } - - QString kickMessage = "Do you wish to kick " + userName + " from your domain"; - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Kick User", kickMessage, - QMessageBox::Yes | QMessageBox::No); - - if (dlg->getDialogItem()) { - - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - bool yes = (static_cast(answer.toInt()) == QMessageBox::Yes); - // ask the NodeList to kick the user with the given session ID - - if (yes) { - DependencyManager::get()->kickNodeBySessionID(nodeID); - } - - _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = false; }); - }); - - _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = true; }); + if (_kickConfirmationOperator) { + bool waitingForKickResponse = _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); + if (getCanKick() && !waitingForKickResponse) { + _kickConfirmationOperator(nodeID); } + } else { + DependencyManager::get()->kickNodeBySessionID(nodeID); } } diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 0e3f9be0e0..f8ca974b8b 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -39,6 +39,12 @@ class UsersScriptingInterface : public QObject, public Dependency { public: UsersScriptingInterface(); + void setKickConfirmationOperator(std::function kickConfirmationOperator) { + _kickConfirmationOperator = kickConfirmationOperator; + } + + bool getWaitForKickResponse() { return _kickResponseLock.resultWithReadLock([&] { return _waitingForKickResponse; }); } + void setWaitForKickResponse(bool waitForKickResponse) { _kickResponseLock.withWriteLock([&] { _waitingForKickResponse = waitForKickResponse; }); } public slots: @@ -197,6 +203,8 @@ private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); + std::function _kickConfirmationOperator; + ReadWriteLockable _kickResponseLock; bool _waitingForKickResponse { false }; }; From fe28eaca7cca368c481eabb6b5ad8dd5a3af9b60 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 18 Mar 2019 11:23:48 -0700 Subject: [PATCH 086/117] fix typo --- interface/resources/qml/hifi/NameCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index c92afe9e14..4e578f8274 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -96,7 +96,7 @@ Item { enabled: (selected && activeTab == "nearbyTab") || isMyCard; hoverEnabled: enabled onClicked: { - if (Phas3DHTML) { + if (has3DHTML) { userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; userInfoViewer.visible = true; } From 0990b56952da701f65c47e7160004aebcab00c97 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 16:46:37 -0700 Subject: [PATCH 087/117] Better avoid overwriting textures in ModelBaker::compressTexture --- libraries/baking/src/ModelBaker.cpp | 47 ++++++++++++++--------------- libraries/baking/src/ModelBaker.h | 8 ++--- libraries/baking/src/TextureBaker.h | 2 ++ 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index b1f6e1d51b..977f773337 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -410,43 +410,40 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture } auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull()); - QString baseTextureFileName; - if (_remappedTexturePaths.contains(urlToTexture)) { - baseTextureFileName = _remappedTexturePaths[urlToTexture]; - } else { + TextureKey textureKey { urlToTexture, textureType }; + auto bakingTextureIt = _bakingTextures.find(textureKey); + if (bakingTextureIt == _bakingTextures.cend()) { // 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 - baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); - _remappedTexturePaths[urlToTexture] = baseTextureFileName; - } + QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType); - qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName - << "to" << baseTextureFileName; + QString bakedTextureFilePath { + _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX + }; - QString bakedTextureFilePath { - _bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX - }; + textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX; - - if (!_bakingTextures.contains(urlToTexture)) { _outputFiles.push_back(bakedTextureFilePath); // bake this texture asynchronously - bakeTexture(urlToTexture, textureType, _bakedOutputDir, baseTextureFileName, textureContent); + bakeTexture(textureKey, _bakedOutputDir, baseTextureFileName, textureContent); + } else { + // Fetch existing texture meta name + textureChild = (*bakingTextureIt)->getBaseFilename() + BAKED_META_TEXTURE_SUFFIX; } } + + qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName + << "to" << textureChild; return textureChild; } -void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, - const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { - +void ModelBaker::bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { // start a bake for this texture and add it to our list to keep track of QSharedPointer bakingTexture{ - new TextureBaker(textureURL, textureType, outputDir, "../", bakedFilename, textureContent), + new TextureBaker(textureKey.first, textureKey.second, outputDir, "../", bakedFilename, textureContent), &TextureBaker::deleteLater }; @@ -455,7 +452,7 @@ void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type t connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture); // keep a shared pointer to the baking texture - _bakingTextures.insert(textureURL, bakingTexture); + _bakingTextures.insert(textureKey, bakingTexture); // start baking the texture on one of our available worker threads bakingTexture->moveToThread(_textureThreadGetter()); @@ -507,7 +504,7 @@ void ModelBaker::handleBakedTexture() { // now that this texture has been baked and handled, we can remove that TextureBaker from our hash - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); checkIfTexturesFinished(); } else { @@ -518,7 +515,7 @@ void ModelBaker::handleBakedTexture() { _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()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); // abort any other ongoing texture bakes since we know we'll end up failing for (auto& bakingTexture : _bakingTextures) { @@ -531,7 +528,7 @@ void ModelBaker::handleBakedTexture() { // we have errors to attend to, so we don't do extra processing for this texture // but we do need to remove that TextureBaker from our list // and then check if we're done with all textures - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); checkIfTexturesFinished(); } @@ -545,7 +542,7 @@ void ModelBaker::handleAbortedTexture() { qDebug() << "Texture aborted: " << bakedTexture->getTextureURL(); if (bakedTexture) { - _bakingTextures.remove(bakedTexture->getTextureURL()); + _bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() }); } // since a texture we were baking aborted, our status is also aborted diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 45b0f4c6ca..6b69d4d0ec 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -42,6 +42,8 @@ class ModelBaker : public Baker { Q_OBJECT public: + using TextureKey = QPair; + ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); virtual ~ModelBaker(); @@ -99,13 +101,11 @@ private slots: private: QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false); - void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, - const QString & bakedFilename, const QByteArray & textureContent); + void bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); - QMultiHash> _bakingTextures; + QMultiHash> _bakingTextures; QHash _textureNameMatchCount; - QHash _remappedTexturePaths; bool _pendingErrorEmission { false }; bool _hasBeenBaked { false }; diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index 9b86d875e9..4fc9680653 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -39,6 +39,8 @@ public: QUrl getTextureURL() const { return _textureURL; } + QString getBaseFilename() const { return _baseFilename; } + QString getMetaTextureFileName() const { return _metaTextureFileName; } virtual void setWasAborted(bool wasAborted) override; From bc3b35aad337938dad06bc908877c5eda560053a Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 09:42:45 -0700 Subject: [PATCH 088/117] Do not consolidate source images by file for now, since they may have the same filename but different paths --- libraries/baking/src/TextureBaker.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index dfc684ddee..d097b4765b 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -48,18 +48,24 @@ TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type tex _baseFilename = originalFilename.left(originalFilename.lastIndexOf('.')); } - _originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName()); + auto textureFilename = _textureURL.fileName(); + QString originalExtension; + int extensionStart = textureFilename.indexOf("."); + if (extensionStart != -1) { + originalExtension = textureFilename.mid(extensionStart); + } + _originalCopyFilePath = _outputDirectory.absoluteFilePath(_baseFilename + originalExtension); } void TextureBaker::bake() { // once our texture is loaded, kick off a the processing connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture); - if (_originalTexture.isEmpty() && !QFile(_originalCopyFilePath.toString()).exists()) { + if (_originalTexture.isEmpty()) { // first load the texture (either locally or remotely) loadTexture(); } else { - // we already have a texture passed to us, or the texture is already saved, so use that + // we already have a texture passed to us, use that emit originalTextureLoaded(); } } @@ -135,7 +141,7 @@ void TextureBaker::processTexture() { // Copy the original file into the baked output directory if it doesn't exist yet { QFile file { originalCopyFilePath }; - if (!file.exists() && (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1)) { + if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) { handleError("Could not write original texture for " + _textureURL.toString()); return; } From c29b3a8c351aae118df25e918bcccee9a4620976 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 10:20:10 -0700 Subject: [PATCH 089/117] Bypass signals in JSBaker/MaterialBaker when resource is already loaded --- libraries/baking/src/JSBaker.cpp | 2 +- libraries/baking/src/MaterialBaker.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index e5682cde20..96d7247a82 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -36,7 +36,7 @@ void JSBaker::bake() { loadScript(); } else { // we already have a script passed to us, use that - emit originalScriptLoaded(); + processScript(); } } diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 47604fa7dc..458e8ad482 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -57,7 +57,7 @@ void MaterialBaker::bake() { } else { // we already have a material passed to us, use that if (_materialResource->isLoaded()) { - emit originalMaterialLoaded(); + processMaterial(); } else { connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); } From cf40ed953bf6aa6cc7413cefb065d371f546b16c Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 11:56:34 -0700 Subject: [PATCH 090/117] Do not create temporary directory in ModelBaker and copy model directly to the original output folder --- libraries/baking/src/ModelBaker.cpp | 73 ++++++++---------------- libraries/baking/src/ModelBaker.h | 4 +- libraries/baking/src/baking/FSTBaker.cpp | 12 ++-- 3 files changed, 30 insertions(+), 59 deletions(-) diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 977f773337..9568a81578 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -50,38 +50,12 @@ ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter input _textureThreadGetter(inputTextureThreadGetter), _hasBeenBaked(hasBeenBaked) { - auto tempDir = PathUtils::generateTemporaryDir(); - - if (tempDir.isEmpty()) { - handleError("Failed to create a temporary directory."); - return; - } - - _modelTempDir = tempDir; - - _originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName()); - qDebug() << "Made temporary dir " << _modelTempDir; - qDebug() << "Origin file path: " << _originalModelFilePath; - - { - auto bakedFilename = _modelURL.fileName(); - if (!hasBeenBaked) { - bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.')); - bakedFilename += BAKED_FBX_EXTENSION; - } - _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; - } -} - -ModelBaker::~ModelBaker() { - if (_modelTempDir.exists()) { - if (!_modelTempDir.remove(_originalModelFilePath)) { - qCWarning(model_baking) << "Failed to remove temporary copy of model file:" << _originalModelFilePath; - } - if (!_modelTempDir.rmdir(".")) { - qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir; - } + auto bakedFilename = _modelURL.fileName(); + if (!hasBeenBaked) { + bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.')); + bakedFilename += BAKED_FBX_EXTENSION; } + _bakedModelURL = _bakedOutputDir + "/" + bakedFilename; } void ModelBaker::setOutputURLSuffix(const QUrl& outputURLSuffix) { @@ -136,7 +110,8 @@ void ModelBaker::initializeOutputDirs() { } } - if (QDir(_originalOutputDir).exists()) { + QDir originalOutputDir { _originalOutputDir }; + if (originalOutputDir.exists()) { if (_mappingURL.isEmpty()) { qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing."; } @@ -144,8 +119,16 @@ void ModelBaker::initializeOutputDirs() { qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir; if (!QDir().mkpath(_originalOutputDir)) { handleError("Failed to create original output folder " + _originalOutputDir); + return; } } + + if (originalOutputDir.isReadable()) { + // The output directory is available. Use that to write/read the original model file + _originalOutputModelPath = originalOutputDir.filePath(_modelURL.fileName()); + } else { + handleError("Unable to write to original output folder " + _originalOutputDir); + } } void ModelBaker::saveSourceModel() { @@ -154,7 +137,7 @@ void ModelBaker::saveSourceModel() { // load up the local file QFile localModelURL { _modelURL.toLocalFile() }; - qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; + qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalOutputModelPath; if (!localModelURL.exists()) { //QMessageBox::warning(this, "Could not find " + _modelURL.toString(), ""); @@ -162,13 +145,7 @@ void ModelBaker::saveSourceModel() { return; } - // make a copy in the output folder - if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); - localModelURL.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - - localModelURL.copy(_originalModelFilePath); + localModelURL.copy(_originalOutputModelPath); // emit our signal to start the import of the model source copy emit modelLoaded(); @@ -199,13 +176,13 @@ void ModelBaker::handleModelNetworkReply() { qCDebug(model_baking) << "Downloaded" << _modelURL; // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalModelFilePath); + QFile copyOfOriginal(_originalOutputModelPath); - qDebug(model_baking) << "Writing copy of original model file to" << _originalModelFilePath << copyOfOriginal.fileName(); + qDebug(model_baking) << "Writing copy of original model file to" << _originalOutputModelPath << copyOfOriginal.fileName(); if (!copyOfOriginal.open(QIODevice::WriteOnly)) { // add an error to the error list for this model stating that a duplicate of the original model could not be made - handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalOutputModelPath + ")"); return; } if (copyOfOriginal.write(requestReply->readAll()) == -1) { @@ -216,10 +193,6 @@ void ModelBaker::handleModelNetworkReply() { // close that file now that we are done writing to it copyOfOriginal.close(); - if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); - } - // emit our signal to start the import of the model source copy emit modelLoaded(); } else { @@ -229,9 +202,9 @@ void ModelBaker::handleModelNetworkReply() { } void ModelBaker::bakeSourceCopy() { - QFile modelFile(_originalModelFilePath); + QFile modelFile(_originalOutputModelPath); if (!modelFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); + handleError("Error opening " + _originalOutputModelPath + " for reading"); return; } hifi::ByteArray modelData = modelFile.readAll(); @@ -243,7 +216,7 @@ void ModelBaker::bakeSourceCopy() { { auto serializer = DependencyManager::get()->getSerializerForMediaType(modelData, _modelURL, ""); if (!serializer) { - handleError("Could not recognize file type of model file " + _originalModelFilePath); + handleError("Could not recognize file type of model file " + _originalOutputModelPath); return; } hifi::VariantHash serializerMapping = _mapping; diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 6b69d4d0ec..d9a559392f 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -46,7 +46,6 @@ public: ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false); - virtual ~ModelBaker(); void setOutputURLSuffix(const QUrl& urlSuffix); void setMappingURL(const QUrl& mappingURL); @@ -86,10 +85,9 @@ protected: QString _bakedOutputDir; QString _originalOutputDir; TextureBakerThreadGetter _textureThreadGetter; + QString _originalOutputModelPath; QString _outputMappingURL; QUrl _bakedModelURL; - QDir _modelTempDir; - QString _originalModelFilePath; protected slots: void handleModelNetworkReply(); diff --git a/libraries/baking/src/baking/FSTBaker.cpp b/libraries/baking/src/baking/FSTBaker.cpp index f76180bb58..acf3bfe1c7 100644 --- a/libraries/baking/src/baking/FSTBaker.cpp +++ b/libraries/baking/src/baking/FSTBaker.cpp @@ -49,9 +49,9 @@ void FSTBaker::bakeSourceCopy() { return; } - QFile fstFile(_originalModelFilePath); + QFile fstFile(_originalOutputModelPath); if (!fstFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalModelFilePath + " for reading"); + handleError("Error opening " + _originalOutputModelPath + " for reading"); return; } @@ -60,25 +60,25 @@ void FSTBaker::bakeSourceCopy() { auto filenameField = _mapping[FILENAME_FIELD].toString(); if (filenameField.isEmpty()) { - handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be found"); + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be found"); return; } auto modelURL = _mappingURL.adjusted(QUrl::RemoveFilename).resolved(filenameField); auto bakeableModelURL = getBakeableModelURL(modelURL); if (bakeableModelURL.isEmpty()) { - handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalModelFilePath + "' could not be resolved to a valid bakeable model url"); + handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be resolved to a valid bakeable model url"); return; } auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir); _modelBaker = std::unique_ptr(dynamic_cast(baker.release())); if (!_modelBaker) { - handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); + handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker"); return; } if (dynamic_cast(_modelBaker.get())) { // Could be interesting, but for now let's just prevent infinite FST loops in the most straightforward way possible - handleError("The FST file '" + _originalModelFilePath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported."); + handleError("The FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported."); return; } _modelBaker->setMappingURL(_mappingURL); From 266f3a8ad8cbe90c5a87dcce3fb19ef9b781e45c Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 12:09:35 -0700 Subject: [PATCH 091/117] Warn if handleFinishedTextureBaker doesn't recognize the sender --- libraries/baking/src/MaterialBaker.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 458e8ad482..dd1ba55e54 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -193,6 +193,8 @@ void MaterialBaker::handleFinishedTextureBaker() { if (_textureBakers.empty()) { outputMaterial(); } + } else { + handleWarning("Unidentified baker finished and signaled to material baker to handle texture. Material: " + _materialData); } } From 64fbf51ac25d8708e9e3410ac2ac4b45195a8601 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 13:13:12 -0700 Subject: [PATCH 092/117] Warn if baked mesh is empty for OBJBaker --- libraries/baking/src/OBJBaker.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp index ebc24201f4..70bdeb2071 100644 --- a/libraries/baking/src/OBJBaker.cpp +++ b/libraries/baking/src/OBJBaker.cpp @@ -119,7 +119,7 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h // Store the draco node containing the compressed mesh information, along with the per-meshPart material IDs the draco node references // Because we redefine the material IDs when initializing the material nodes above, we pass that in for the material list // The nth mesh part gets the nth material - { + if (!dracoMesh.isEmpty()) { std::vector newMaterialList; newMaterialList.reserve(_materialIDs.size()); for (auto materialID : _materialIDs) { @@ -128,6 +128,8 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h FBXNode dracoNode; buildDracoMeshNode(dracoNode, dracoMesh, newMaterialList); geometryNode.children.append(dracoNode); + } else { + handleWarning("Baked mesh for OBJ model '" + _modelURL.toString() + "' is empty"); } // Generating Texture Node From e4cafced2ad7afb0421538a3d6e64b42b492466b Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 13:42:07 -0700 Subject: [PATCH 093/117] Re-name GRAP_KEY to GRAB_KEY in DomainBaker.cpp --- tools/oven/src/DomainBaker.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 544786f03e..3970238ab5 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -302,7 +302,7 @@ const QString TYPE_KEY = "type"; // Models const QString MODEL_URL_KEY = "modelURL"; const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL"; -const QString GRAP_KEY = "grab"; +const QString GRAB_KEY = "grab"; const QString EQUIPPABLE_INDICATOR_URL_KEY = "equippableIndicatorURL"; const QString ANIMATION_KEY = "animation"; const QString ANIMATION_URL_KEY = "url"; @@ -354,10 +354,10 @@ void DomainBaker::enumerateEntities() { addModelBaker(ANIMATION_KEY + "." + ANIMATION_URL_KEY, animationObject[ANIMATION_URL_KEY].toString(), *it); } } - if (entity.contains(GRAP_KEY)) { - auto grabObject = entity[GRAP_KEY].toObject(); + if (entity.contains(GRAB_KEY)) { + auto grabObject = entity[GRAB_KEY].toObject(); if (grabObject.contains(EQUIPPABLE_INDICATOR_URL_KEY)) { - addModelBaker(GRAP_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it); + addModelBaker(GRAB_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it); } } From e6487332e8c5c458067e0c3943b64c4269585045 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 14:25:34 -0700 Subject: [PATCH 094/117] Attempt to fix linker error with Android and draco in BuildDracoMeshTask.cpp --- libraries/model-baker/src/model-baker/Baker.cpp | 2 ++ .../model-baker/src/model-baker/BuildDracoMeshTask.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 7bb53376ed..536255a841 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -164,6 +164,8 @@ namespace baker { // Build Draco meshes // NOTE: This task is disabled by default and must be enabled through configuration // TODO: Tangent support (Needs changes to FBXSerializer_Mesh as well) + // NOTE: Due to an unresolved linker error, BuildDracoMeshTask is not functional on Android + // TODO: Figure out why BuildDracoMeshTask.cpp won't link with draco on Android const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); const auto dracoMeshes = buildDracoMeshOutputs.getN(0); diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 46b170fd25..2e378965de 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -22,8 +22,11 @@ #pragma GCC diagnostic ignored "-Wsign-compare" #endif + +#ifndef Q_OS_ANDROID #include #include +#endif #ifdef _WIN32 #pragma warning( pop ) @@ -35,6 +38,7 @@ #include "ModelBakerLogging.h" #include "ModelMath.h" +#ifndef Q_OS_ANDROID std::vector createMaterialList(const hfm::Mesh& mesh) { std::vector materialList; for (const auto& meshPart : mesh.parts) { @@ -199,6 +203,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v return dracoMesh; } +#endif // not Q_OS_ANDROID void BuildDracoMeshTask::configure(const Config& config) { _encodeSpeed = config.encodeSpeed; @@ -206,6 +211,9 @@ void BuildDracoMeshTask::configure(const Config& config) { } void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { +#ifdef Q_OS_ANDROID + qCWarning(model_baker) << "BuildDracoMesh is disabled on Android. Output meshes will be empty."; +#else const auto& meshes = input.get0(); const auto& normalsPerMesh = input.get1(); const auto& tangentsPerMesh = input.get2(); @@ -239,4 +247,5 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp dracoBytes = hifi::ByteArray(buffer.data(), (int)buffer.size()); } } +#endif // not Q_OS_ANDROID } From b0b4307f27cfae415552067e0c2870b526abf376 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 15:58:25 -0700 Subject: [PATCH 095/117] Fix unsupported model types preventing DomainBaker from finishing --- tools/oven/src/DomainBaker.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 3970238ab5..e21b1e4435 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -176,12 +176,12 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // 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(bakeableModelURL, { property, jsonRef }); } } - - // 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(bakeableModelURL, { property, jsonRef }); } } From fa6a94f16a4c65e817dde0a04fa099345e51d1b8 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Mon, 18 Mar 2019 17:21:05 -0700 Subject: [PATCH 096/117] Fix not mapping identical URLs in DomainBaker --- tools/oven/src/DomainBaker.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index e21b1e4435..05745aad24 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -150,7 +150,8 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con QUrl bakeableModelURL = getBakeableModelURL(url); if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) { // setup a ModelBaker for this URL, as long as we don't already have one - if (!_modelBakers.contains(bakeableModelURL)) { + bool haveBaker = _modelBakers.contains(bakeableModelURL); + if (!haveBaker) { auto getWorkerThreadCallback = []() -> QThread* { return Oven::instance().getNextWorkerThread(); }; @@ -168,6 +169,7 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // insert it into our bakers hash so we hold a strong pointer to it _modelBakers.insert(bakeableModelURL, baker); + haveBaker = true; // move the baker to the baker thread // and kickoff the bake @@ -176,12 +178,14 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // 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(bakeableModelURL, { property, jsonRef }); } } + + if (haveBaker) { + // 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(bakeableModelURL, { property, jsonRef }); + } } } From b6e583087e16a768f389c22f414acd35ad9382e2 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 18 Mar 2019 17:40:01 -0700 Subject: [PATCH 097/117] Stand-alone Tagging: tagged places not coming first in GOTO list --- interface/resources/qml/hifi/Feed.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 718ebc9331..a9fde05d8d 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -54,7 +54,7 @@ Column { 'require_online=true', 'protocol=' + encodeURIComponent(Window.protocolSignature()) ]; - endpoint: '/api/v1/user_stories?' + options.join('&'); + endpoint: '/api/v1/user_stories?' + options.join('&') + (PlatformInfo.isStandalone() ? '&standalone_optimized=true' : '') itemsPerPage: 4; processPage: function (data) { return data.user_stories.map(makeModelData); From 0e4d3b2aeb07b93729d9b55ad5ea485ac89c5b9a Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 18 Mar 2019 17:40:54 -0700 Subject: [PATCH 098/117] allow baseURL to be specified in fst --- libraries/model-networking/src/model-networking/ModelCache.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index a48f96eb1b..d588b711c9 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -120,6 +120,8 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { if (filename.isNull()) { finishedLoading(false); } else { + const QString baseURL = _mapping.value("baseURL").toString(); + _url = _effectiveBaseURL.resolved(baseURL); QUrl url = _url.resolved(filename); QString texdir = _mapping.value(TEXDIR_FIELD).toString(); From 718eed8d5b8494046b970ded5f5fb36a18be64f2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 19 Mar 2019 09:58:44 -0700 Subject: [PATCH 099/117] Corrected typo. --- cmake/macros/TargetPython.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/macros/TargetPython.cmake b/cmake/macros/TargetPython.cmake index cd0ea0f34c..2c055cf8bc 100644 --- a/cmake/macros/TargetPython.cmake +++ b/cmake/macros/TargetPython.cmake @@ -1,7 +1,7 @@ macro(TARGET_PYTHON) if (NOT HIFI_PYTHON_EXEC) # Find the python interpreter - if (CAME_VERSION VERSION_LESS 3.12) + if (CMAKE_VERSION VERSION_LESS 3.12) # this logic is deprecated in CMake after 3.12 # FIXME eventually we should make 3.12 the min cmake verion and just use the Python3 find_package path set(Python_ADDITIONAL_VERSIONS 3) From 61fb65b5a42209b2baf378715f8b71af43777738 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 19 Mar 2019 12:41:58 -0700 Subject: [PATCH 100/117] don't set _url, so that cache_clearing works --- .../src/model-networking/ModelCache.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index d588b711c9..6f4cbfa253 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -121,20 +121,20 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { finishedLoading(false); } else { const QString baseURL = _mapping.value("baseURL").toString(); - _url = _effectiveBaseURL.resolved(baseURL); - QUrl url = _url.resolved(filename); + const QUrl base = _effectiveBaseURL.resolved(baseURL); + QUrl url = base.resolved(filename); QString texdir = _mapping.value(TEXDIR_FIELD).toString(); if (!texdir.isNull()) { if (!texdir.endsWith('/')) { texdir += '/'; } - _textureBaseUrl = resolveTextureBaseUrl(url, _url.resolved(texdir)); + _textureBaseUrl = resolveTextureBaseUrl(url, base.resolved(texdir)); } else { _textureBaseUrl = url.resolved(QUrl(".")); } - auto scripts = FSTReader::getScripts(_url, _mapping); + auto scripts = FSTReader::getScripts(base, _mapping); if (scripts.size() > 0) { _mapping.remove(SCRIPT_FIELD); for (auto &scriptPath : scripts) { @@ -147,7 +147,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { if (animGraphVariant.isValid()) { QUrl fstUrl(animGraphVariant.toString()); if (fstUrl.isValid()) { - _animGraphOverrideUrl = _url.resolved(fstUrl); + _animGraphOverrideUrl = base.resolved(fstUrl); } else { _animGraphOverrideUrl = QUrl(); } @@ -156,7 +156,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { } auto modelCache = DependencyManager::get(); - GeometryExtra extra { GeometryMappingPair(_url, _mapping), _textureBaseUrl, false }; + GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseUrl, false }; // Get the raw GeometryResource _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); From 51ab8880e93eeaa927d0267417e731615a47c139 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 19 Mar 2019 13:25:40 -0700 Subject: [PATCH 101/117] Corrected labels. --- tools/nitpick/ui/Nitpick.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index a0f368863d..e1d7699f22 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -46,7 +46,7 @@ - 5 + 0 @@ -620,7 +620,7 @@ <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> - usePreviousInstallation + Use Previous Installation false @@ -895,7 +895,7 @@ <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> - usePreviousInstallation + Use Previous Installation false From 94739aa7c7377f7c3cf5a4ade17082e7e2fa8f16 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 13:32:44 -0700 Subject: [PATCH 102/117] Case 21769 - updatable items not showing up as updatable after clicking 'show updates' button. --- .../resources/qml/hifi/commerce/purchases/PurchasedItem.qml | 2 +- interface/resources/qml/hifi/commerce/purchases/Purchases.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index a7b36eae36..0e3402a6a9 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -49,7 +49,7 @@ Item { property string wornEntityID; property string updatedItemId; property string upgradeTitle; - property bool updateAvailable: root.updateItemId && root.updateItemId !== ""; + property bool updateAvailable: root.updateItemId !== ""; property bool valid; property bool standaloneOptimized; property bool standaloneIncompatible; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 46bbb626c6..9a2bf62e08 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -523,9 +523,9 @@ Rectangle { item.cardBackVisible = false; item.isInstalled = root.installedApps.indexOf(item.id) > -1; item.wornEntityID = ''; + item.upgrade_id = item.upgrade_id ? item.upgrade_id : ""; }); sendToScript({ method: 'purchases_updateWearables' }); - return data.assets; } } @@ -545,7 +545,7 @@ Rectangle { delegate: PurchasedItem { itemName: title; itemId: id; - updateItemId: model.upgrade_id ? model.upgrade_id : ""; + updateItemId: model.upgrade_id itemPreviewImageUrl: preview; itemHref: download_url; certificateId: certificate_id; From 775eddc2657df459e76b365cade537fc2252ee0f Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 19 Mar 2019 16:16:53 -0700 Subject: [PATCH 103/117] Agent requires the ModelCache singleton for zone entities w/ meshes --- assignment-client/src/Agent.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 5c644cb132..3937d5f799 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -52,6 +52,8 @@ #include #include // TODO: consider moving to scriptengine.h +#include + #include "entities/AssignmentParentFinder.h" #include "AssignmentDynamicFactory.h" #include "RecordingScriptingInterface.h" @@ -99,6 +101,9 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + // Needed to ensure the creation of the DebugDraw instance on the main thread DebugDraw::getInstance(); @@ -819,6 +824,9 @@ void Agent::aboutToFinish() { DependencyManager::get()->cleanup(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); // cleanup the AudioInjectorManager (and any still running injectors) From 19c51b25d1d28878fbe692322774e62905d2fea9 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 19 Mar 2019 22:38:16 +0100 Subject: [PATCH 104/117] don't ignore updates that originate from entityPropertiesTool itself --- scripts/system/edit.js | 7 ++++--- scripts/system/html/js/entityProperties.js | 2 +- scripts/system/libraries/entitySelectionTool.js | 9 ++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 2c3785217c..ca2918a108 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2286,14 +2286,15 @@ var PropertiesTool = function (opts) { }) }; - function updateSelections(selectionUpdated) { + function updateSelections(selectionUpdated, caller) { if (blockPropertyUpdates) { return; } var data = { type: 'update', - spaceMode: selectionDisplay.getSpaceMode() + spaceMode: selectionDisplay.getSpaceMode(), + isPropertiesToolUpdate: caller === this, }; if (selectionUpdated) { @@ -2339,7 +2340,7 @@ var PropertiesTool = function (opts) { emitScriptEvent(data); } - selectionManager.addEventListener(updateSelections); + selectionManager.addEventListener(updateSelections, this); var onWebEventReceived = function(data) { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index f501df7933..f259b0a017 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -3326,7 +3326,7 @@ function loaded() { let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; - if (!hasSelectedEntityChanged && document.hasFocus()) { + if (!data.isPropertiesToolUpdate && !hasSelectedEntityChanged && document.hasFocus()) { // in case the selection has not changed and we still have focus on the properties page, // we will ignore the event. return; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 269283ea6d..064dafec06 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -128,8 +128,11 @@ SelectionManager = (function() { } }; - that.addEventListener = function(func) { - listeners.push(func); + that.addEventListener = function(func, thisContext) { + listeners.push({ + callback: func, + thisContext: thisContext + }); }; that.hasSelection = function() { @@ -572,7 +575,7 @@ SelectionManager = (function() { for (var j = 0; j < listeners.length; j++) { try { - listeners[j](selectionUpdated === true, caller); + listeners[j].callback.call(listeners[j].thisContext, selectionUpdated === true, caller); } catch (e) { print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e)); } From 640b05304a8a72abe2591e157da6470edf9f7931 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 16:24:07 -0700 Subject: [PATCH 105/117] Case 21392 - Excise unneeded code from marketplacesInject.js --- scripts/system/html/js/marketplacesInject.js | 365 ------------------- 1 file changed, 365 deletions(-) diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 8d408169ba..56075a514e 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -28,10 +28,6 @@ var xmlHttpRequest = null; var isPreparing = false; // Explicitly track download request status. - var limitedCommerce = false; - var commerceMode = false; - var userIsLoggedIn = false; - var walletNeedsSetup = false; var marketplaceBaseURL = "https://highfidelity.com"; var messagesWaiting = false; @@ -109,356 +105,6 @@ }); } - emitWalletSetupEvent = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "WALLET_SETUP" - })); - }; - - function maybeAddSetupWalletButton() { - if (!$('body').hasClass("walletsetup-injected") && userIsLoggedIn && walletNeedsSetup) { - $('body').addClass("walletsetup-injected"); - - var resultsElement = document.getElementById('results'); - var setupWalletElement = document.createElement('div'); - setupWalletElement.classList.add("row"); - setupWalletElement.id = "setupWalletDiv"; - setupWalletElement.style = "height:60px;margin:20px 10px 10px 10px;padding:12px 5px;" + - "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; - - var span = document.createElement('span'); - span.style = "margin:10px 5px;color:#1b6420;font-size:15px;"; - span.innerHTML = "Activate your Wallet to get money and shop in Marketplace."; - - var xButton = document.createElement('a'); - xButton.id = "xButton"; - xButton.setAttribute('href', "#"); - xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; - xButton.innerHTML = "X"; - xButton.onclick = function () { - setupWalletElement.remove(); - dummyRow.remove(); - }; - - setupWalletElement.appendChild(span); - setupWalletElement.appendChild(xButton); - - resultsElement.insertBefore(setupWalletElement, resultsElement.firstChild); - - // Dummy row for padding - var dummyRow = document.createElement('div'); - dummyRow.classList.add("row"); - dummyRow.style = "height:15px;"; - resultsElement.insertBefore(dummyRow, resultsElement.firstChild); - } - } - - function maybeAddLogInButton() { - if (!$('body').hasClass("login-injected") && !userIsLoggedIn) { - $('body').addClass("login-injected"); - var resultsElement = document.getElementById('results'); - if (!resultsElement) { // If we're on the main page, this will evaluate to `true` - resultsElement = document.getElementById('item-show'); - resultsElement.style = 'margin-top:0;'; - } - var logInElement = document.createElement('div'); - logInElement.classList.add("row"); - logInElement.id = "logInDiv"; - logInElement.style = "height:60px;margin:20px 10px 10px 10px;padding:5px;" + - "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; - - var button = document.createElement('a'); - button.classList.add("btn"); - button.classList.add("btn-default"); - button.id = "logInButton"; - button.setAttribute('href', "#"); - button.innerHTML = "LOG IN"; - button.style = "width:80px;height:100%;margin-top:0;margin-left:10px;padding:13px;font-weight:bold;background:linear-gradient(white, #ccc);"; - button.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "LOGIN" - })); - }; - - var span = document.createElement('span'); - span.style = "margin:10px;color:#1b6420;font-size:15px;"; - span.innerHTML = "to get items from the Marketplace."; - - var xButton = document.createElement('a'); - xButton.id = "xButton"; - xButton.setAttribute('href', "#"); - xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; - xButton.innerHTML = "X"; - xButton.onclick = function () { - logInElement.remove(); - dummyRow.remove(); - }; - - logInElement.appendChild(button); - logInElement.appendChild(span); - logInElement.appendChild(xButton); - - resultsElement.insertBefore(logInElement, resultsElement.firstChild); - - // Dummy row for padding - var dummyRow = document.createElement('div'); - dummyRow.classList.add("row"); - dummyRow.style = "height:15px;"; - resultsElement.insertBefore(dummyRow, resultsElement.firstChild); - } - } - - function changeDropdownMenu() { - var logInOrOutButton = document.createElement('a'); - logInOrOutButton.id = "logInOrOutButton"; - logInOrOutButton.setAttribute('href', "#"); - logInOrOutButton.innerHTML = userIsLoggedIn ? "Log Out" : "Log In"; - logInOrOutButton.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "LOGIN" - })); - }; - - $($('.dropdown-menu').find('li')[0]).append(logInOrOutButton); - - $('a[href="/marketplace?view=mine"]').each(function () { - $(this).attr('href', '#'); - $(this).on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "MY_ITEMS" - })); - }); - }); - } - - function buyButtonClicked(id, referrer, edition) { - EventBridge.emitWebEvent(JSON.stringify({ - type: "CHECKOUT", - itemId: id, - referrer: referrer, - itemEdition: edition - })); - } - - function injectBuyButtonOnMainPage() { - var cost; - - // Unbind original mouseenter and mouseleave behavior - $('body').off('mouseenter', '#price-or-edit .price'); - $('body').off('mouseleave', '#price-or-edit .price'); - - $('.grid-item').find('#price-or-edit').each(function () { - $(this).css({ "margin-top": "0" }); - }); - - $('.grid-item').find('#price-or-edit').find('a').each(function() { - if ($(this).attr('href') !== '#') { // Guard necessary because of the AJAX nature of Marketplace site - $(this).attr('data-href', $(this).attr('href')); - $(this).attr('href', '#'); - } - cost = $(this).closest('.col-xs-3').find('.item-cost').text(); - var costInt = parseInt(cost, 10); - - $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); - $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); - - var priceElement = $(this).find('.price'); - var available = true; - - if (priceElement.text() === 'invalidated' || - priceElement.text() === 'sold out' || - priceElement.text() === 'not for sale') { - available = false; - priceElement.css({ - "padding": "3px 5px 10px 5px", - "height": "40px", - "background": "linear-gradient(#a2a2a2, #fefefe)", - "color": "#000", - "font-weight": "600", - "line-height": "34px" - }); - } else { - priceElement.css({ - "padding": "3px 5px", - "height": "40px", - "background": "linear-gradient(#00b4ef, #0093C5)", - "color": "#FFF", - "font-weight": "600", - "line-height": "34px" - }); - } - - if (parseInt(cost) > 0) { - priceElement.css({ "width": "auto" }); - - if (available) { - priceElement.html(' ' + cost); - } - - priceElement.css({ "min-width": priceElement.width() + 30 }); - } - }); - - // change pricing to GET/BUY on button hover - $('body').on('mouseenter', '#price-or-edit .price', function () { - var $this = $(this); - var buyString = "BUY"; - var getString = "GET"; - // Protection against the button getting stuck in the "BUY"/"GET" state. - // That happens when the browser gets two MOUSEENTER events before getting a - // MOUSELEAVE event. Also, if not available for sale, just return. - if ($this.text() === buyString || - $this.text() === getString || - $this.text() === 'invalidated' || - $this.text() === 'sold out' || - $this.text() === 'not for sale' ) { - return; - } - $this.data('initialHtml', $this.html()); - - var cost = $(this).parent().siblings().text(); - if (parseInt(cost) > 0) { - $this.text(buyString); - } - if (parseInt(cost) == 0) { - $this.text(getString); - } - }); - - $('body').on('mouseleave', '#price-or-edit .price', function () { - var $this = $(this); - $this.html($this.data('initialHtml')); - }); - - - $('.grid-item').find('#price-or-edit').find('a').on('click', function () { - var price = $(this).closest('.grid-item').find('.price').text(); - if (price === 'invalidated' || - price === 'sold out' || - price === 'not for sale') { - return false; - } - buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), - "mainPage", - -1); - }); - } - - function injectUnfocusOnSearch() { - // unfocus input field on search, thus hiding virtual keyboard - $('#search-box').on('submit', function () { - if (document.activeElement) { - document.activeElement.blur(); - } - }); - } - - // fix for 10108 - marketplace category cannot scroll - function injectAddScrollbarToCategories() { - $('#categories-dropdown').on('show.bs.dropdown', function () { - $('body > div.container').css('display', 'none') - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }); - }); - - $('#categories-dropdown').on('hide.bs.dropdown', function () { - $('body > div.container').css('display', ''); - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }); - }); - } - - function injectHiFiCode() { - if (commerceMode) { - maybeAddLogInButton(); - maybeAddSetupWalletButton(); - - if (!$('body').hasClass("code-injected")) { - - $('body').addClass("code-injected"); - changeDropdownMenu(); - - var target = document.getElementById('templated-items'); - // MutationObserver is necessary because the DOM is populated after the page is loaded. - // We're searching for changes to the element whose ID is '#templated-items' - this is - // the element that gets filled in by the AJAX. - var observer = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - injectBuyButtonOnMainPage(); - }); - }); - var config = { attributes: true, childList: true, characterData: true }; - observer.observe(target, config); - - // Try this here in case it works (it will if the user just pressed the "back" button, - // since that doesn't trigger another AJAX request. - injectBuyButtonOnMainPage(); - } - } - - injectUnfocusOnSearch(); - injectAddScrollbarToCategories(); - } - - function injectHiFiItemPageCode() { - if (commerceMode) { - maybeAddLogInButton(); - - if (!$('body').hasClass("code-injected")) { - - $('body').addClass("code-injected"); - changeDropdownMenu(); - - var purchaseButton = $('#side-info').find('.btn').first(); - - var href = purchaseButton.attr('href'); - purchaseButton.attr('href', '#'); - var cost = $('.item-cost').text(); - var costInt = parseInt(cost, 10); - var availability = $.trim($('.item-availability').text()); - if (limitedCommerce && (costInt > 0)) { - availability = ''; - } - if (availability === 'available') { - purchaseButton.css({ - "background": "linear-gradient(#00b4ef, #0093C5)", - "color": "#FFF", - "font-weight": "600", - "padding-bottom": "10px" - }); - } else { - purchaseButton.css({ - "background": "linear-gradient(#a2a2a2, #fefefe)", - "color": "#000", - "font-weight": "600", - "padding-bottom": "10px" - }); - } - - var type = $('.item-type').text(); - var isUpdating = window.location.href.indexOf('edition=') > -1; - var urlParams = new URLSearchParams(window.location.search); - if (isUpdating) { - purchaseButton.html('UPDATE FOR FREE'); - } else if (availability !== 'available') { - purchaseButton.html('UNAVAILABLE ' + (availability ? ('(' + availability + ')') : '')); - } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { - purchaseButton.html('PURCHASE ' + cost); - } - - purchaseButton.on('click', function () { - if ('available' === availability || isUpdating) { - buyButtonClicked(window.location.pathname.split("/")[3], - "itemPage", - urlParams.get('edition')); - } - }); - } - } - - injectUnfocusOnSearch(); - } - function updateClaraCode() { // Have to repeatedly update Clara page because its content can change dynamically without location.href changing. @@ -695,16 +341,9 @@ case DIRECTORY: injectDirectoryCode(); break; - case HIFI: - injectHiFiCode(); - break; case CLARA: injectClaraCode(); break; - case HIFI_ITEM_PAGE: - injectHiFiItemPageCode(); - break; - } } @@ -717,10 +356,6 @@ cancelClaraDownload(); } else if (message.type === "marketplaces") { if (message.action === "commerceSetting") { - limitedCommerce = !!message.data.limitedCommerce; - commerceMode = !!message.data.commerceMode; - userIsLoggedIn = !!message.data.userIsLoggedIn; - walletNeedsSetup = !!message.data.walletNeedsSetup; marketplaceBaseURL = message.data.metaverseServerURL; if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); From ab61f65ea2897bec5ed2973c6a5825e73603fbbc Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 17:08:04 -0700 Subject: [PATCH 106/117] CR fix --- interface/resources/qml/hifi/NameCard.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 4e578f8274..141ddf0077 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -368,7 +368,7 @@ Item { enabled: selected hoverEnabled: true onClicked: { - if(has3DHTML) { + if (has3DHTML) { userInfoViewer.url = Account.metaverseServerURL + "/users/" + userName; userInfoViewer.visible = true; } From 27fa0dc4c683d699233085a0d67d62a0729e83bc Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 19 Mar 2019 17:24:19 -0700 Subject: [PATCH 107/117] CR fixes --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index a9f058fce1..3402f919fb 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -359,7 +359,7 @@ Rectangle { } onAccepted: { - if(root.searchString !== searchField.text) { + if (root.searchString !== searchField.text) { root.searchString = searchField.text; getMarketplaceItems(); searchField.forceActiveFocus(); From 9d11e44b4bed1ed0ea32b2c62f28f452719336da Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 20 Mar 2019 11:37:16 -0700 Subject: [PATCH 108/117] update AvatarEntity trait when parentID changes --- interface/src/avatar/MyAvatar.cpp | 23 +++++++++++-------- interface/src/avatar/MyAvatar.h | 2 +- .../src/avatars-renderer/Avatar.cpp | 2 +- .../src/avatars-renderer/Avatar.h | 2 +- .../entities/src/EntityEditPacketSender.cpp | 11 ++------- .../entities/src/EntityEditPacketSender.h | 4 ++-- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 02ef91cdba..ddedc270f8 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1570,7 +1570,7 @@ void MyAvatar::handleChangedAvatarEntityData() { entityTree->withWriteLock([&] { EntityItemPointer entity = entityTree->addEntity(id, properties); if (entity) { - packetSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, properties); + packetSender->queueEditAvatarEntityMessage(entityTree, id); } }); } @@ -3451,10 +3451,10 @@ float MyAvatar::getGravity() { } void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { - QUuid oldID = getSessionUUID(); + QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); - QUuid id = getSessionUUID(); - if (id != oldID) { + QUuid newSessionID = getSessionUUID(); + if (newSessionID != oldSessionID) { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { @@ -3462,15 +3462,20 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); }); + EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); entityTree->withWriteLock([&] { for (const auto& entityID : avatarEntityIDs) { auto entity = entityTree->findEntityByID(entityID); if (!entity) { continue; } - entity->setOwningAvatarID(id); - if (entity->getParentID() == oldID) { - entity->setParentID(id); + entity->setOwningAvatarID(newSessionID); + // NOTE: each attached AvatarEntity should already have the correct updated parentID + // via magic in SpatiallyNestable, but when an AvatarEntity IS parented to MyAvatar + // we need to update the "packedAvatarEntityData" we send to the avatar-mixer + // so that others will get the updated state. + if (entity->getParentID() == newSessionID) { + packetSender->queueEditAvatarEntityMessage(entityTree, entityID); } } }); @@ -5523,14 +5528,14 @@ void MyAvatar::initFlowFromFST() { } } -void MyAvatar::sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { +void MyAvatar::sendPacket(const QUuid& entityID) const { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { entityTree->withWriteLock([&] { // force an update packet EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); - packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entityID, properties); + packetSender->queueEditAvatarEntityMessage(entityTree, entityID); }); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index aadc8ee268..905216cfba 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1918,7 +1918,7 @@ private: bool didTeleport(); bool getIsAway() const { return _isAway; } void setAway(bool value); - void sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const override; + void sendPacket(const QUuid& entityID) const override; std::mutex _pinnedJointsMutex; std::vector _pinnedJoints; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 38108416ee..992ee5db96 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -376,7 +376,7 @@ bool Avatar::applyGrabChanges() { const EntityItemPointer& entity = std::dynamic_pointer_cast(target); if (entity && entity->getEntityHostType() == entity::HostType::AVATAR && entity->getSimulationOwner().getID() == getID()) { EntityItemProperties properties = entity->getProperties(); - sendPacket(entity->getID(), properties); + sendPacket(entity->getID()); } } } else { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index d81b04d4b2..6026367440 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -605,7 +605,7 @@ protected: // protected methods... bool isLookingAtMe(AvatarSharedPointer avatar) const; - virtual void sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { } + virtual void sendPacket(const QUuid& entityID) const { } bool applyGrabChanges(); void relayJointDataToChildren(); diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index af0e34303b..0491bdedae 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -39,9 +39,7 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte } } -void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer entityTree, - EntityItemID entityItemID, - const EntityItemProperties& properties) { +void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer entityTree, EntityItemID entityItemID) { assert(_myAvatar); if (!entityTree) { qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage null entityTree."; @@ -54,11 +52,6 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer enti } entity->setLastBroadcast(usecTimestampNow()); - // serialize ALL properties in an "AvatarEntity" packet - // rather than just the ones being edited. - EntityItemProperties entityProperties = entity->getProperties(); - entityProperties.merge(properties); - OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE); EncodeBitstreamParams params; EntityTreeElementExtraEncodeDataPointer extra { nullptr }; @@ -82,7 +75,7 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit with no myAvatar"; } else if (properties.getOwningAvatarID() == _myAvatar->getID()) { // this is an avatar-based entity --> update our avatar-data rather than sending to the entity-server - queueEditAvatarEntityMessage(entityTree, entityItemID, properties); + queueEditAvatarEntityMessage(entityTree, entityItemID); } else { qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit for another avatar"; } diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 99a5202986..3cc2f016f0 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -50,8 +50,8 @@ public slots: void processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode); private: - void queueEditAvatarEntityMessage(EntityTreePointer entityTree, - EntityItemID entityItemID, const EntityItemProperties& properties); + friend class MyAvatar; + void queueEditAvatarEntityMessage(EntityTreePointer entityTree, EntityItemID entityItemID); private: std::mutex _mutex; From b12a2684649c7ab2382dbb958452869f3e03e1f6 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 20 Mar 2019 16:25:59 -0700 Subject: [PATCH 109/117] Fixed copy-paste-drink too much error --- tools/nitpick/src/AWSInterface.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index 16c0a220d8..19697d51dc 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -53,10 +53,6 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, _testResults = testResults; - _urlLineEdit = urlLineEdit; - _urlLineEdit->setEnabled(false); - - _urlLineEdit = urlLineEdit; _urlLineEdit->setEnabled(false); From 5888adfd9035090cfc84737d6d5917f371b87d08 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 20 Mar 2019 17:08:42 -0700 Subject: [PATCH 110/117] update terms of service links --- interface/resources/qml/LoginDialog/CompleteProfileBody.qml | 4 ++-- interface/resources/qml/LoginDialog/SignUpBody.qml | 2 +- interface/resources/qml/LoginDialog/UsernameCollisionBody.qml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index 65f8a8c1dc..17d6a7d3b3 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -379,9 +379,9 @@ Item { Component.onCompleted: { // with the link. if (completeProfileBody.withOculus) { - termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") } else { - termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") } } } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 64df9089a1..69ac2f5a6c 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -395,7 +395,7 @@ Item { text: signUpBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") } } diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index 2c8e61a29a..d450b1e7bc 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -218,7 +218,7 @@ Item { text: usernameCollisionBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") } } From c777f94231045cfc35de064fa8d79f6d47f9f1ee Mon Sep 17 00:00:00 2001 From: Robin Wilson Date: Tue, 19 Mar 2019 16:14:55 -0700 Subject: [PATCH 111/117] remove AvatarBookmarks.deleteBookmark function --- interface/src/AvatarBookmarks.cpp | 3 +++ interface/src/AvatarBookmarks.h | 3 +++ interface/src/Bookmarks.h | 5 +---- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 5fe35bd23f..54c67daab8 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -149,6 +149,9 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { emit bookmarkDeleted(bookmarkName); } +void AvatarBookmarks::deleteBookmark() { +} + void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) { auto myAvatar = DependencyManager::get()->getMyAvatar(); auto currentAvatarEntities = myAvatar->getAvatarEntityData(); diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 4623e7d929..df75fec865 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -76,6 +76,9 @@ protected: void readFromFile() override; QVariantMap getAvatarDataToBookmark(); +protected slots: + void deleteBookmark() override; + private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; const QString ENTRY_AVATAR_URL = "avatarUrl"; diff --git a/interface/src/Bookmarks.h b/interface/src/Bookmarks.h index 88510e4eda..56d26b55c6 100644 --- a/interface/src/Bookmarks.h +++ b/interface/src/Bookmarks.h @@ -51,13 +51,10 @@ protected: bool _isMenuSorted; protected slots: - /**jsdoc - * @function AvatarBookmarks.deleteBookmark - */ /**jsdoc * @function LocationBookmarks.deleteBookmark */ - void deleteBookmark(); + virtual void deleteBookmark(); private: static bool sortOrder(QAction* a, QAction* b); From 87d75ec75ceee653cfb974ec49d92547c99409ca Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 21 Mar 2019 12:08:42 -0700 Subject: [PATCH 112/117] Case 20617 - People app filter bar breaks when deleting connections --- interface/resources/qml/hifi/Pal.qml | 8 ++++++++ scripts/system/pal.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 1c190a2b79..55f2bb80b1 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -1261,6 +1261,14 @@ Rectangle { case 'refreshConnections': refreshConnections(); break; + case 'connectionRemoved': + for (var i=0; i Date: Thu, 21 Mar 2019 15:00:11 -0700 Subject: [PATCH 113/117] don't queue AvatarEntity messages when not in domain --- interface/src/avatar/MyAvatar.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ddedc270f8..298e661f24 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3454,6 +3454,11 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); QUuid newSessionID = getSessionUUID(); + if (DependencyManager::get()->getSessionUUID().isNull()) { + // we don't actually have a connection to a domain right now + // so there is no need to queue AvatarEntity messages --> bail early + return; + } if (newSessionID != oldSessionID) { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; From 9a14cfc7dfbeb63a56796bd55dad82579a87b613 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 21 Mar 2019 20:50:59 +0100 Subject: [PATCH 114/117] make sure that onWebEventReceived has the correct context in VR --- scripts/system/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index ca2918a108..894ea2b696 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2524,7 +2524,7 @@ var PropertiesTool = function (opts) { createToolsWindow.webEventReceived.addListener(this, onWebEventReceived); - webView.webEventReceived.connect(onWebEventReceived); + webView.webEventReceived.connect(this, onWebEventReceived); return that; }; From 4c7d5c7da7cd876933be0fbe45a247021245f7ba Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Mar 2019 12:19:20 +1300 Subject: [PATCH 115/117] Detect signal functions based on their return type --- tools/jsdoc/plugins/hifi.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index b4350ddbdb..bd77204347 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -121,6 +121,11 @@ exports.handlers = { e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn; } } + + if (e.doclet.kind === "function" && e.doclet.returns && e.doclet.returns[0].type + && e.doclet.returns[0].type.names[0] === "Signal") { + e.doclet.kind = "signal"; + } } }; @@ -178,4 +183,4 @@ exports.defineTags = function (dictionary) { } }); -}; \ No newline at end of file +}; From 025326b85f2a85924386033acd486f15a2a2eb61 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Mar 2019 12:20:10 +1300 Subject: [PATCH 116/117] Remove custom @signal tag --- tools/jsdoc/plugins/hifi.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index bd77204347..b2b91de1c8 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -129,20 +129,6 @@ exports.handlers = { } }; -// Functions for adding @signal custom tag -/** @private */ -function setDocletKindToTitle(doclet, tag) { - doclet.addTag( 'kind', tag.title ); -} - -function setDocletNameToValue(doclet, tag) { - if (tag.value && tag.value.description) { // as in a long tag - doclet.addTag('name', tag.value.description); - } else if (tag.text) { // or a short tag - doclet.addTag('name', tag.text); - } -} - // Define custom hifi tags here exports.defineTags = function (dictionary) { @@ -173,14 +159,5 @@ exports.defineTags = function (dictionary) { doclet.hifiServerEntity = true; } }); - - // @signal - dictionary.defineTag("signal", { - mustHaveValue: true, - onTagged: function(doclet, tag) { - setDocletKindToTitle(doclet, tag); - setDocletNameToValue(doclet, tag); - } - }); }; From ba0923a3ad7a44550257b5196e3f793a1a8898b0 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 22 Mar 2019 12:20:22 +1300 Subject: [PATCH 117/117] Fix signal summary text --- tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl index b9a0e0ca86..c5fdefc7d8 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl @@ -13,8 +13,8 @@ var self = this; - - + +

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 B?KS`a literal 74623 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 vsaK=d2 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 From 79dda7a6c77108d7dda6252769660f9d7a8815b2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 13:42:33 -0700 Subject: [PATCH 077/117] build errors --- libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp | 1 - libraries/entities-renderer/src/RenderableShapeEntityItem.cpp | 2 -- libraries/entities-renderer/src/RenderableTextEntityItem.cpp | 2 -- libraries/graphics/src/graphics/MaterialTextures.slh | 2 +- libraries/render-utils/src/DeferredBufferWrite.slh | 2 +- 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 46a810f6a4..38108416ee 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -43,7 +43,6 @@ using namespace std; -const int NUM_BODY_CONE_SIDES = 9; const float CHAT_MESSAGE_SCALE = 0.0015f; const float CHAT_MESSAGE_HEIGHT = 0.1f; const float DISPLAYNAME_FADE_TIME = 0.5f; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 0ba3adbe9b..b33eb619c8 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -19,8 +19,6 @@ #include "RenderPipelines.h" -#include - //#define SHAPE_ENTITY_USE_FADE_EFFECT #ifdef SHAPE_ENTITY_USE_FADE_EFFECT #include diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 5cd0abae68..a3e1a2f56d 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -19,8 +19,6 @@ #include "GLMHelpers.h" -#include - #include "DeferredLightingEffect.h" using namespace render; diff --git a/libraries/graphics/src/graphics/MaterialTextures.slh b/libraries/graphics/src/graphics/MaterialTextures.slh index 1cbee33238..c725aae9bb 100644 --- a/libraries/graphics/src/graphics/MaterialTextures.slh +++ b/libraries/graphics/src/graphics/MaterialTextures.slh @@ -235,7 +235,7 @@ vec3 fetchLightmapMap(vec2 uv) { <@endfunc@> <@func discardInvisible(opacity)@> { - if (<$opacity$> < 1.e-6) { + if (<$opacity$> <= 0.0) { discard; } } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 66d0aa2ddb..fc9310a520 100644 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -64,7 +64,7 @@ void packDeferredFragmentUnlit(vec3 normal, float alpha, vec3 color) { } void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, float roughness) { - if (alpha < 1.e-6) { + if (alpha <= 0.0) { discard; } _fragColor0 = vec4(albedo.rgb, alpha); From 60ed9e12a42b77e89d24d14536c0328a359221cf Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 15:17:33 -0700 Subject: [PATCH 078/117] Attempt to fix build errors --- libraries/baking/src/MaterialBaker.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index b2392e0cb7..47604fa7dc 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -27,6 +27,15 @@ std::function MaterialBaker::_getNextOvenWorkerThreadOperator; static int materialNum = 0; +namespace std { + template <> + struct hash { + size_t operator()(const graphics::Material::MapChannel& a) const { + return std::hash()((size_t)a); + } + }; +}; + MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) : _materialData(materialData), _isURL(isURL), From 0d9403051574d6bffd3ab4343ec08d17d78dd5da Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 15:32:31 -0700 Subject: [PATCH 079/117] use web surface alpha --- libraries/render-utils/src/simple_opaque_web_browser.slf | 2 +- libraries/render-utils/src/simple_transparent_web_browser.slf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/simple_opaque_web_browser.slf b/libraries/render-utils/src/simple_opaque_web_browser.slf index 36b0c825ad..df789ee22b 100644 --- a/libraries/render-utils/src/simple_opaque_web_browser.slf +++ b/libraries/render-utils/src/simple_opaque_web_browser.slf @@ -28,7 +28,7 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord1 _texCoord01.zw void main(void) { - vec4 texel = texture(originalTexture, _texCoord0.st); + vec4 texel = texture(originalTexture, _texCoord0); texel = color_sRGBAToLinear(texel); packDeferredFragmentUnlit(normalize(_normalWS), 1.0, _color.rgb * texel.rgb); } diff --git a/libraries/render-utils/src/simple_transparent_web_browser.slf b/libraries/render-utils/src/simple_transparent_web_browser.slf index 1d5aad0914..599fd3d87f 100644 --- a/libraries/render-utils/src/simple_transparent_web_browser.slf +++ b/libraries/render-utils/src/simple_transparent_web_browser.slf @@ -28,11 +28,11 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; #define _texCoord1 _texCoord01.zw void main(void) { - vec4 texel = texture(originalTexture, _texCoord0.st); + vec4 texel = texture(originalTexture, _texCoord0); texel = color_sRGBAToLinear(texel); packDeferredFragmentTranslucent( normalize(_normalWS), - _color.a, + _color.a * texel.a, _color.rgb * texel.rgb, DEFAULT_ROUGHNESS); } From 53429f459e2c7094239f0abf6ca3e38dfdceb65d Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 15:32:39 -0700 Subject: [PATCH 080/117] Remove some redundancy involving model texture URL resolution --- libraries/baking/src/FBXBaker.h | 1 - libraries/baking/src/ModelBaker.cpp | 15 ++++++--------- libraries/baking/src/ModelBaker.h | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index 257efbe983..59ef5e349d 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -43,7 +43,6 @@ private: void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList); hfm::Model::Pointer _hfmModel; - QHash _remappedTexturePaths; bool _pendingErrorEmission { false }; }; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 0a5341cce4..b1f6e1d51b 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -408,7 +408,7 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture if (!modelTextureFileInfo.filePath().isEmpty()) { textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit()); } - auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull()); + auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull()); QString baseTextureFileName; if (_remappedTexturePaths.contains(urlToTexture)) { @@ -559,14 +559,11 @@ void ModelBaker::handleAbortedTexture() { checkIfTexturesFinished(); } -QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) { +QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded) { QUrl urlToTexture; - // use QFileInfo to easily split up the existing texture filename into its components - auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); - if (isEmbedded) { - urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath(); + urlToTexture = _modelURL.toString() + "/" + textureFileInfo.filePath(); } else { if (textureFileInfo.exists() && textureFileInfo.isFile()) { // set the texture URL to the local texture that we have confirmed exists @@ -576,14 +573,14 @@ QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativ // this is a relative file path which will require different handling // depending on the location of the original model - if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + if (_modelURL.isLocalFile() && textureFileInfo.exists() && textureFileInfo.isFile()) { // the absolute path we ran into for the texture in the model exists on this machine // so use that file - urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); + urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); } else { // we didn't find the texture on this machine at the absolute path // so assume that it is right beside the model to match the behaviour of interface - urlToTexture = _modelURL.resolved(apparentRelativePath.fileName()); + urlToTexture = _modelURL.resolved(textureFileInfo.fileName()); } } } diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 17af2604a2..45b0f4c6ca 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -98,7 +98,7 @@ private slots: void handleAbortedTexture(); private: - QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); + QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false); void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, const QString & bakedFilename, const QByteArray & textureContent); QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); From b6c44ea4436e95ccbace9dad8e06fd7bd3c64ee0 Mon Sep 17 00:00:00 2001 From: sabrina-shanman Date: Fri, 15 Mar 2019 15:56:36 -0700 Subject: [PATCH 081/117] Remove unused variables when iterating through mesh nodes in FBXBaker --- libraries/baking/src/FBXBaker.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 371a492964..2189e7bdc3 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -123,21 +123,7 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const } } else if (object.name == "Model") { for (FBXNode& modelChild : object.children) { - bool properties = false; - hifi::ByteArray propertyName; - int index; - if (modelChild.name == "Properties60") { - properties = true; - propertyName = "Property"; - index = 3; - - } else if (modelChild.name == "Properties70") { - properties = true; - propertyName = "P"; - index = 4; - } - - if (properties) { + if (modelChild.name == "Properties60" || modelChild.name == "Properties70") { // This is a properties node // Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation"); From d4e1ec97418614d9a88393352c41ce71d8aa5eb2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 15 Mar 2019 15:50:48 -0700 Subject: [PATCH 082/117] fix emitScriptEvent --- interface/src/Application.cpp | 7 +++++++ libraries/entities-renderer/src/RenderableEntityItem.h | 1 + .../entities-renderer/src/RenderableWebEntityItem.h | 2 +- libraries/entities/src/EntityItem.h | 2 -- libraries/entities/src/EntityScriptingInterface.cpp | 9 +-------- libraries/entities/src/EntityScriptingInterface.h | 1 - libraries/entities/src/EntityTree.cpp | 7 +++++++ libraries/entities/src/EntityTree.h | 4 ++++ 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..21ef706dfc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1985,6 +1985,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return nullptr; }); + EntityTree::setEmitScriptEventOperator([this](const QUuid& id, const QVariant& message) { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + entity->emitScriptEvent(message); + } + }); + EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { auto entities = getEntities(); if (auto entity = entities->renderableForEntityId(id)) { diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index e9a6035e3d..39f9ad091e 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -40,6 +40,7 @@ public: virtual bool wantsKeyboardFocus() const { return false; } virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } + virtual void emitScriptEvent(const QVariant& message) {} const EntityItemPointer& getEntity() const { return _entity; } const ItemID& getRenderItemID() const { return _renderItemID; } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 0345898b62..7118774d30 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -106,7 +106,7 @@ private: static std::function&, bool&, std::vector&)> _releaseWebSurfaceOperator; public slots: - void emitScriptEvent(const QVariant& scriptMessage); + void emitScriptEvent(const QVariant& scriptMessage) override; signals: void scriptEventReceived(const QVariant& message); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a9a8baa413..fae871a124 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -511,8 +511,6 @@ public: virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } - virtual void emitScriptEvent(const QVariant& message) {} - QUuid getLastEditedBy() const { return _lastEditedBy; } void setLastEditedBy(QUuid value) { _lastEditedBy = value; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 22cd26eac6..55a36202a8 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -2202,14 +2202,7 @@ bool EntityScriptingInterface::wantsHandControllerPointerEvents(const QUuid& id) } void EntityScriptingInterface::emitScriptEvent(const EntityItemID& entityID, const QVariant& message) { - if (_entityTree) { - _entityTree->withReadLock([&] { - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - if (entity) { - entity->emitScriptEvent(message); - } - }); - } + EntityTree::emitScriptEvent(entityID, message); } // TODO move this someplace that makes more sense... diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 0cf9070b08..f6aedac3fc 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -1529,7 +1529,6 @@ public slots: * @function Entities.emitScriptEvent * @param {Uuid} entityID - The ID of the {@link Entities.EntityType|Web} entity. * @param {string} message - The message to send. - * @todo This function is currently not implemented. */ Q_INVOKABLE void emitScriptEvent(const EntityItemID& entityID, const QVariant& message); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index d64c8870eb..8bf7c92b1f 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2978,6 +2978,7 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const { std::function EntityTree::_getEntityObjectOperator = nullptr; std::function EntityTree::_textSizeOperator = nullptr; std::function EntityTree::_areEntityClicksCapturedOperator = nullptr; +std::function EntityTree::_emitScriptEventOperator = nullptr; QObject* EntityTree::getEntityObject(const QUuid& id) { if (_getEntityObjectOperator) { @@ -3000,6 +3001,12 @@ bool EntityTree::areEntityClicksCaptured() { return false; } +void EntityTree::emitScriptEvent(const QUuid& id, const QVariant& message) { + if (_emitScriptEventOperator) { + _emitScriptEventOperator(id, message); + } +} + void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, MovingEntitiesOperator& moveOperator, bool force, bool tellServer) { // if the queryBox has changed, tell the entity-server diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 39b3dc57c7..e627a07d13 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -272,6 +272,9 @@ public: static void setEntityClicksCapturedOperator(std::function areEntityClicksCapturedOperator) { _areEntityClicksCapturedOperator = areEntityClicksCapturedOperator; } static bool areEntityClicksCaptured(); + static void setEmitScriptEventOperator(std::function emitScriptEventOperator) { _emitScriptEventOperator = emitScriptEventOperator; } + static void emitScriptEvent(const QUuid& id, const QVariant& message); + std::map getNamedPaths() const { return _namedPaths; } void updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender, @@ -383,6 +386,7 @@ private: static std::function _getEntityObjectOperator; static std::function _textSizeOperator; static std::function _areEntityClicksCapturedOperator; + static std::function _emitScriptEventOperator; std::vector _staleProxies; From e1358fd5f5059d44074aeb3e54836ec06f6d6e17 Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 15 Mar 2019 16:28:46 -0700 Subject: [PATCH 083/117] prevent export from humanoid config, cleaner scene switch --- .../Editor/AvatarExporter/AvatarExporter.cs | 28 ++++++++++-------- .../AvatarExporter/HeightReference.prefab | 10 +++---- tools/unity-avatar-exporter/Assets/README.txt | 4 +-- .../avatarExporter.unitypackage | Bin 74591 -> 74615 bytes 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 87f401d478..11d83a52e8 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -17,7 +17,7 @@ using System.Text.RegularExpressions; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.3.6"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.7"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; @@ -332,8 +332,7 @@ class AvatarExporter : MonoBehaviour { static List alternateStandardShaderMaterials = new List(); static List unsupportedShaderMaterials = new List(); - static Scene previewScene; - static string previousScene = ""; + static SceneSetup[] previousSceneSetup; static Vector3 previousScenePivot = Vector3.zero; static Quaternion previousSceneRotation = Quaternion.identity; static float previousSceneSize = 0.0f; @@ -1223,14 +1222,22 @@ class AvatarExporter : MonoBehaviour { } static bool OpenPreviewScene() { + // store the current scene setup to restore when closing the preview scene + previousSceneSetup = EditorSceneManager.GetSceneManagerSetup(); + + // if the user is currently in the Humanoid Avatar Configuration then inform them to close it first + if (EditorSceneManager.GetActiveScene().name == "Avatar Configuration" && previousSceneSetup.Length == 0) { + EditorUtility.DisplayDialog("Error", "Please exit the Avatar Configuration before exporting.", "Ok"); + return false; + } + // see if the user wants to save their current scene before opening preview avatar scene in place of user's scene if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { return false; } - // store the user's current scene to re-open when done and open a new default scene in place of the user's scene - previousScene = EditorSceneManager.GetActiveScene().path; - previewScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); + // open a new empty scene in place of the user's scene + EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); // instantiate a game object to preview the avatar and a game object for the height reference prefab at 0, 0, 0 UnityEngine.Object heightReferenceResource = AssetDatabase.LoadAssetAtPath(HEIGHT_REFERENCE_PREFAB, typeof(UnityEngine.Object)); @@ -1259,13 +1266,8 @@ class AvatarExporter : MonoBehaviour { DestroyImmediate(avatarPreviewObject); DestroyImmediate(heightReferenceObject); - // re-open the scene the user had open before switching to the preview scene - if (!string.IsNullOrEmpty(previousScene)) { - EditorSceneManager.OpenScene(previousScene); - } - - // close the preview scene and flag it to be removed - EditorSceneManager.CloseScene(previewScene, true); + // restore to the previous scene setup that the user had open before exporting + EditorSceneManager.RestoreSceneManagerSetup(previousSceneSetup); // restore the camera pivot and rotation to the user's previous scene settings var sceneView = SceneView.lastActiveSceneView; diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab index 3a6b6b21fa..4464617387 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/HeightReference.prefab @@ -599,7 +599,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -774,7 +774,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -809,7 +809,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -844,7 +844,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 @@ -879,7 +879,7 @@ MeshRenderer: m_ReflectionProbeUsage: 1 m_RenderingLayerMask: 4294967295 m_Materials: - - {fileID: 2100000, guid: d1133891b03286946b3b0c63c1a57d08, type: 2} + - {fileID: 2100000, guid: 83f430ba33ffd544bab7a13796246d30, type: 2} m_StaticBatchInfo: firstSubMesh: 0 subMeshCount: 0 diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 5b228ebf75..0b5cb49117 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,8 +1,8 @@ High Fidelity, Inc. Avatar Exporter -Version 0.3.6 +Version 0.3.7 -Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. +Note: It is recommended to use Unity versions between 2017.4.15f1 and 2018.2.12f1 for this Avatar Exporter. To create a new avatar project: 1. Import your .fbx avatar model into your Unity project's Assets by either dragging and dropping the file into the Assets window or by using Assets menu > Import New Assets. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 05ad49baa6bfa1696cb12f659750246f2a6d2d9e..d906cfc0a45055b79a0ed8667441799fea682af3 100644 GIT binary patch literal 74615 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 Date: Thu, 14 Mar 2019 17:05:29 -0700 Subject: [PATCH 066/117] prevent warnings on embedded materials --- .../Editor/AvatarExporter/AvatarExporter.cs | 5 +++++ .../avatarExporter.unitypackage | Bin 74582 -> 74600 bytes 2 files changed, 5 insertions(+) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs index 142e4ae35a..f0d970031c 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs @@ -1105,6 +1105,11 @@ class AvatarExporter : MonoBehaviour { string materialName = material.name; string shaderName = material.shader.name; + // if this material isn't mapped externally then ignore it + if (!materialMappings.ContainsValue(materialName)) { + continue; + } + // don't store any material data for unsupported shader types if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) { if (!unsupportedShaderMaterials.Contains(materialName)) { diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index ee3f6abe01b509bba63360b2834400660f9f3901..48a9502079839c3aee59ead39531198dd5d4bf86 100644 GIT binary patch delta 72937 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 L^b|O508|42fq(tw delta 72877 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 z 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%0Dyirf01r8FqLONO!VuD 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