From a1d18feb0a88fbe5f95dedbb0a32e88c9f76b955 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Thu, 31 Jan 2019 16:30:02 -0300 Subject: [PATCH 01/43] Set message dialog text width so wrap mode takes effect --- interface/resources/qml/dialogs/MessageDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 9428e3ab6e..5632b319bb 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -95,6 +95,7 @@ ModalWindow { id: mainTextContainer onTextChanged: d.resize(); wrapMode: Text.WordWrap + width: messageBox.width size: hifi.fontSizes.menuItem color: hifi.colors.baseGrayHighlight anchors { From e601f6c59f798c1730fe24c54abc27842e451b6b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 7 Feb 2019 14:10:09 -0800 Subject: [PATCH 02/43] move material mapping to hfm prep step --- libraries/fbx/src/FBXSerializer.cpp | 2 +- libraries/fbx/src/FBXSerializer.h | 2 +- libraries/fbx/src/FBXSerializer_Material.cpp | 27 +-------- .../model-baker/ApplyMaterialMappingTask.cpp | 55 +++++++++++++++++++ .../model-baker/ApplyMaterialMappingTask.h | 27 +++++++++ .../model-baker/src/model-baker/Baker.cpp | 10 +++- 6 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp create mode 100644 libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.h diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 207ee2982d..9e7f422b40 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1326,7 +1326,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr hfmModel.meshExtents.reset(); // Create the Material Library - consolidateHFMMaterials(mapping); + consolidateHFMMaterials(); // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index b95bb729e7..379b1ac743 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -153,7 +153,7 @@ public: QHash _hfmMaterials; QHash _materialParams; - void consolidateHFMMaterials(const QVariantHash& mapping); + void consolidateHFMMaterials(); bool _loadLightmaps { true }; float _lightmapOffset { 0.0f }; diff --git a/libraries/fbx/src/FBXSerializer_Material.cpp b/libraries/fbx/src/FBXSerializer_Material.cpp index 9caf713e75..b47329e483 100644 --- a/libraries/fbx/src/FBXSerializer_Material.cpp +++ b/libraries/fbx/src/FBXSerializer_Material.cpp @@ -75,15 +75,7 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& ma return texture; } -void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { - QJsonObject materialMap; - if (mapping.contains("materialMap")) { - QByteArray materialMapValue = mapping.value("materialMap").toByteArray(); - materialMap = QJsonDocument::fromJson(materialMapValue).object(); - if (materialMap.isEmpty()) { - qCDebug(modelformat) << "fbx Material Map found but did not produce valid JSON:" << materialMapValue; - } - } +void FBXSerializer::consolidateHFMMaterials() { for (QHash::iterator it = _hfmMaterials.begin(); it != _hfmMaterials.end(); it++) { HFMMaterial& material = (*it); @@ -266,23 +258,6 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { } qCDebug(modelformat) << " fbx material Name:" << material.name; - if (materialMap.contains(material.name)) { - QJsonObject materialOptions = materialMap.value(material.name).toObject(); - qCDebug(modelformat) << "Mapping fbx material:" << material.name << " with HifiMaterial: " << materialOptions; - - if (materialOptions.contains("scattering")) { - float scattering = (float) materialOptions.value("scattering").toDouble(); - material._material->setScattering(scattering); - } - - if (materialOptions.contains("scatteringMap")) { - QByteArray scatteringMap = materialOptions.value("scatteringMap").toVariant().toByteArray(); - material.scatteringTexture = HFMTexture(); - material.scatteringTexture.name = material.name + ".scatteringMap"; - material.scatteringTexture.filename = scatteringMap; - } - } - if (material.opacity <= 0.0f) { material._material->setOpacity(1.0f); } else { diff --git a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp new file mode 100644 index 0000000000..c9b4fec1e1 --- /dev/null +++ b/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp @@ -0,0 +1,55 @@ +// +// Created by Sam Gondelman on 2/7/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 "ApplyMaterialMappingTask.h" + +#include "ModelBakerLogging.h" + +void ApplyMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { + const auto& materialsIn = input.get0(); + const auto& mapping = input.get1(); + + auto materialsOut = materialsIn; + + auto mappingIter = mapping.find("materialMap"); + if (mappingIter != mapping.end()) { + QByteArray materialMapValue = mappingIter.value().toByteArray(); + QJsonObject materialMap = QJsonDocument::fromJson(materialMapValue).object(); + if (materialMap.isEmpty()) { + qCDebug(model_baker) << "Material Map found but did not produce valid JSON:" << materialMapValue; + } else { + for (auto& material : materialsOut) { + auto materialMapIter = materialMap.find(material.name); + if (materialMapIter != materialMap.end()) { + QJsonObject materialOptions = materialMapIter.value().toObject(); + qCDebug(model_baker) << "Mapping material:" << material.name << " with HFMaterial: " << materialOptions; + + { + auto scatteringIter = materialOptions.find("scattering"); + if (scatteringIter != materialOptions.end()) { + float scattering = (float)scatteringIter.value().toDouble(); + material._material->setScattering(scattering); + } + } + + { + auto scatteringMapIter = materialOptions.find("scatteringMap"); + if (scatteringMapIter != materialOptions.end()) { + QByteArray scatteringMap = scatteringMapIter.value().toVariant().toByteArray(); + material.scatteringTexture = HFMTexture(); + material.scatteringTexture.name = material.name + ".scatteringMap"; + material.scatteringTexture.filename = scatteringMap; + } + } + } + } + } + } + + output = materialsOut; +} diff --git a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.h new file mode 100644 index 0000000000..271c80fe67 --- /dev/null +++ b/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.h @@ -0,0 +1,27 @@ +// +// Created by Sam Gondelman on 2/7/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_ApplyMaterialMappingTask_h +#define hifi_ApplyMaterialMappingTask_h + +#include + +#include + +#include "Engine.h" + +class ApplyMaterialMappingTask { +public: + using Input = baker::VaryingSet2, QVariantHash>; + using Output = QHash; + using JobModel = baker::Job::ModelIO; + + void run(const baker::BakeContextPointer& context, const Input& input, Output& output); +}; + +#endif // hifi_ApplyMaterialMappingTask_h \ No newline at end of file diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 1c2a2f5c63..981d799f12 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -20,6 +20,7 @@ #include "CalculateBlendshapeNormalsTask.h" #include "CalculateBlendshapeTangentsTask.h" #include "PrepareJointsTask.h" +#include "ApplyMaterialMappingTask.h" namespace baker { @@ -101,7 +102,7 @@ namespace baker { class BuildModelTask { public: - using Input = VaryingSet5, std::vector, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; + using Input = VaryingSet6, std::vector, QMap, QHash, QHash>; using Output = hfm::Model::Pointer; using JobModel = Job::ModelIO; @@ -111,6 +112,7 @@ namespace baker { hfmModelOut->joints = QVector::fromStdVector(input.get2()); hfmModelOut->jointRotationOffsets = input.get3(); hfmModelOut->jointIndices = input.get4(); + hfmModelOut->materials = input.get5(); output = hfmModelOut; } }; @@ -154,12 +156,16 @@ namespace baker { const auto jointRotationOffsets = jointInfoOut.getN(1); const auto jointIndices = jointInfoOut.getN(2); + // Apply material mapping + const auto materialMappingInputs = ApplyMaterialMappingTask::Input(materials, mapping).asVarying(); + const auto materialsOut = model.addJob("ApplyMaterialMapping", materialMappingInputs); + // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs); const auto buildMeshesInputs = BuildMeshesTask::Input(meshesIn, graphicsMeshes, normalsPerMesh, tangentsPerMesh, blendshapesPerMeshOut).asVarying(); const auto meshesOut = model.addJob("BuildMeshes", buildMeshesInputs); - const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying(); + const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, materialsOut).asVarying(); hfmModelOut = model.addJob("BuildModel", buildModelInputs); } }; From d034b080f775587ccb6e1d76672d44937968ae93 Mon Sep 17 00:00:00 2001 From: Cristian Duarte Date: Fri, 8 Feb 2019 14:19:44 -0300 Subject: [PATCH 03/43] Increase min width so texts like protocol mismatch fit and let the OK appear --- interface/resources/qml/dialogs/MessageDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 5632b319bb..6e576118d8 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -75,7 +75,7 @@ ModalWindow { QtObject { id: d - readonly property int minWidth: 480 + readonly property int minWidth: 800 readonly property int maxWidth: 1280 readonly property int minHeight: 120 readonly property int maxHeight: 720 From f7a487a02060cdb9ea5c9468cc48c1b1f4001808 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 8 Feb 2019 10:28:07 -0800 Subject: [PATCH 04/43] move materialcache et al to material-networking library --- assignment-client/CMakeLists.txt | 2 +- interface/CMakeLists.txt | 4 +- interface/src/Application.cpp | 4 +- libraries/avatars-renderer/CMakeLists.txt | 2 +- libraries/display-plugins/CMakeLists.txt | 1 + libraries/entities-renderer/CMakeLists.txt | 2 +- libraries/entities/CMakeLists.txt | 2 +- libraries/entities/src/MaterialEntityItem.h | 2 +- libraries/graphics-scripting/CMakeLists.txt | 2 +- libraries/material-networking/CMakeLists.txt | 5 + .../src/material-networking}/KTXCache.cpp | 0 .../src/material-networking}/KTXCache.h | 0 .../material-networking}/MaterialCache.cpp | 306 ++++++++++++++++++ .../src/material-networking/MaterialCache.h | 115 +++++++ .../MaterialNetworkingLogging.cpp | 11 + .../MaterialNetworkingLogging.h | 11 + .../src/material-networking}/ShaderCache.cpp | 0 .../src/material-networking}/ShaderCache.h | 0 .../src/material-networking}/TextureCache.cpp | 8 +- .../src/material-networking}/TextureCache.h | 0 .../TextureCacheScriptingInterface.cpp | 0 .../TextureCacheScriptingInterface.h | 0 libraries/model-baker/CMakeLists.txt | 3 +- .../model-baker/ApplyMaterialMappingTask.cpp | 2 + libraries/model-networking/CMakeLists.txt | 3 +- .../src/model-networking/MaterialCache.h | 60 ---- .../src/model-networking/ModelCache.cpp | 306 ------------------ .../src/model-networking/ModelCache.h | 61 +--- .../ModelNetworkingLogging.cpp | 2 +- libraries/physics/CMakeLists.txt | 1 + libraries/procedural/CMakeLists.txt | 2 +- .../procedural/src/procedural/Procedural.h | 4 +- libraries/render-utils/CMakeLists.txt | 2 +- .../render-utils/src/RenderPipelines.cpp | 2 +- libraries/render-utils/src/TextureCache.h | 2 +- libraries/script-engine/CMakeLists.txt | 2 +- plugins/openvr/CMakeLists.txt | 2 +- 37 files changed, 480 insertions(+), 451 deletions(-) create mode 100644 libraries/material-networking/CMakeLists.txt rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/KTXCache.cpp (100%) rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/KTXCache.h (100%) rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/MaterialCache.cpp (63%) create mode 100644 libraries/material-networking/src/material-networking/MaterialCache.h create mode 100644 libraries/material-networking/src/material-networking/MaterialNetworkingLogging.cpp create mode 100644 libraries/material-networking/src/material-networking/MaterialNetworkingLogging.h rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/ShaderCache.cpp (100%) rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/ShaderCache.h (100%) rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/TextureCache.cpp (99%) rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/TextureCache.h (100%) rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/TextureCacheScriptingInterface.cpp (100%) rename libraries/{model-networking/src/model-networking => material-networking/src/material-networking}/TextureCacheScriptingInterface.h (100%) delete mode 100644 libraries/model-networking/src/model-networking/MaterialCache.h diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index f28dc90b7d..b7afc3ed9e 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -14,7 +14,7 @@ link_hifi_libraries( audio avatars octree gpu graphics shaders fbx hfm entities networking animation recording shared script-engine embedded-webserver controllers physics plugins midi image - model-networking ktx shaders + material-networking model-networking ktx shaders ) add_dependencies(${TARGET_NAME} oven) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index c013cfacd3..d5dfce957f 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -205,8 +205,8 @@ endif() # link required hifi libraries link_hifi_libraries( shared workload task octree ktx gpu gl procedural graphics graphics-scripting render - pointers - recording hfm fbx networking model-networking model-baker entities avatars trackers + pointers recording hfm fbx networking material-networking + model-networking model-baker entities avatars trackers audio audio-client animation script-engine physics render-utils entities-renderer avatars-renderer ui qml auto-updater midi controllers plugins image trackers diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index daf2dd6363..03b2e6c18e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -102,7 +102,7 @@ #include #include #include -#include +#include #include #include #include @@ -153,7 +153,7 @@ #include #include #include -#include +#include #include "recording/ClipCache.h" #include "AudioClient.h" diff --git a/libraries/avatars-renderer/CMakeLists.txt b/libraries/avatars-renderer/CMakeLists.txt index 06a3804ece..de1ac1a7c2 100644 --- a/libraries/avatars-renderer/CMakeLists.txt +++ b/libraries/avatars-renderer/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME avatars-renderer) setup_hifi_library(Network Script) -link_hifi_libraries(shared shaders gpu graphics animation model-networking script-engine render render-utils image trackers entities-renderer) +link_hifi_libraries(shared shaders gpu graphics animation material-networking model-networking script-engine render render-utils image trackers entities-renderer) include_hifi_library_headers(avatars) include_hifi_library_headers(networking) include_hifi_library_headers(hfm) diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index 8e966ed9ea..ad6503b22d 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -2,6 +2,7 @@ set(TARGET_NAME display-plugins) setup_hifi_library(Gui) link_hifi_libraries(shared shaders plugins ui-plugins gl ui render-utils ${PLATFORM_GL_BACKEND}) include_hifi_library_headers(gpu) +include_hifi_library_headers(material-networking) include_hifi_library_headers(model-networking) include_hifi_library_headers(networking) include_hifi_library_headers(graphics) diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index b08856e8a8..e1896cf674 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME entities-renderer) setup_hifi_library(Network Script) -link_hifi_libraries(shared workload gpu shaders procedural graphics model-networking script-engine render render-utils image qml ui pointers) +link_hifi_libraries(shared workload gpu shaders procedural graphics material-networking model-networking script-engine render render-utils image qml ui pointers) include_hifi_library_headers(networking) include_hifi_library_headers(gl) include_hifi_library_headers(ktx) diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt index fcbe563f88..044d25f77e 100644 --- a/libraries/entities/CMakeLists.txt +++ b/libraries/entities/CMakeLists.txt @@ -6,4 +6,4 @@ include_hifi_library_headers(fbx) include_hifi_library_headers(gpu) include_hifi_library_headers(image) include_hifi_library_headers(ktx) -link_hifi_libraries(shared shaders networking octree avatars graphics model-networking) +link_hifi_libraries(shared shaders networking octree avatars graphics material-networking model-networking) diff --git a/libraries/entities/src/MaterialEntityItem.h b/libraries/entities/src/MaterialEntityItem.h index ba142d7719..069c71c1d6 100644 --- a/libraries/entities/src/MaterialEntityItem.h +++ b/libraries/entities/src/MaterialEntityItem.h @@ -13,7 +13,7 @@ #include "MaterialMappingMode.h" #include -#include +#include class MaterialEntityItem : public EntityItem { using Pointer = std::shared_ptr; diff --git a/libraries/graphics-scripting/CMakeLists.txt b/libraries/graphics-scripting/CMakeLists.txt index 0f59fb41f8..9bb34adda1 100644 --- a/libraries/graphics-scripting/CMakeLists.txt +++ b/libraries/graphics-scripting/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME graphics-scripting) setup_hifi_library() -link_hifi_libraries(shared networking graphics fbx image model-networking script-engine) +link_hifi_libraries(shared networking graphics fbx image material-networking model-networking script-engine) include_hifi_library_headers(gpu) diff --git a/libraries/material-networking/CMakeLists.txt b/libraries/material-networking/CMakeLists.txt new file mode 100644 index 0000000000..4ade61230a --- /dev/null +++ b/libraries/material-networking/CMakeLists.txt @@ -0,0 +1,5 @@ +set(TARGET_NAME material-networking) +setup_hifi_library() +link_hifi_libraries(shared shaders networking graphics fbx ktx image gl) +include_hifi_library_headers(gpu) +include_hifi_library_headers(hfm) \ No newline at end of file diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/material-networking/src/material-networking/KTXCache.cpp similarity index 100% rename from libraries/model-networking/src/model-networking/KTXCache.cpp rename to libraries/material-networking/src/material-networking/KTXCache.cpp diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/material-networking/src/material-networking/KTXCache.h similarity index 100% rename from libraries/model-networking/src/model-networking/KTXCache.h rename to libraries/material-networking/src/material-networking/KTXCache.h diff --git a/libraries/model-networking/src/model-networking/MaterialCache.cpp b/libraries/material-networking/src/material-networking/MaterialCache.cpp similarity index 63% rename from libraries/model-networking/src/model-networking/MaterialCache.cpp rename to libraries/material-networking/src/material-networking/MaterialCache.cpp index aaa9767397..bccf1ca0c4 100644 --- a/libraries/model-networking/src/model-networking/MaterialCache.cpp +++ b/libraries/material-networking/src/material-networking/MaterialCache.cpp @@ -426,4 +426,310 @@ QSharedPointer MaterialCache::createResource(const QUrl& url) { QSharedPointer MaterialCache::createResourceCopy(const QSharedPointer& resource) { return QSharedPointer(new NetworkMaterialResource(*resource.staticCast().data()), &Resource::deleter); +} + +NetworkMaterial::NetworkMaterial(const NetworkMaterial& m) : + Material(m), + _textures(m._textures), + _albedoTransform(m._albedoTransform), + _lightmapTransform(m._lightmapTransform), + _lightmapParams(m._lightmapParams), + _isOriginal(m._isOriginal) +{} + +const QString NetworkMaterial::NO_TEXTURE = QString(); + +const QString& NetworkMaterial::getTextureName(MapChannel channel) { + if (_textures[channel].texture) { + return _textures[channel].name; + } + return NO_TEXTURE; +} + +QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const HFMTexture& texture) { + if (texture.content.isEmpty()) { + // External file: search relative to the baseUrl, in case filename is relative + return baseUrl.resolved(QUrl(texture.filename)); + } else { + // Inlined file: cache under the fbx file to avoid namespace clashes + // NOTE: We cannot resolve the path because filename may be an absolute path + assert(texture.filename.size() > 0); + auto baseUrlStripped = baseUrl.toDisplayString(QUrl::RemoveFragment | QUrl::RemoveQuery | QUrl::RemoveUserInfo); + if (texture.filename.at(0) == '/') { + return baseUrlStripped + texture.filename; + } else { + return baseUrlStripped + '/' + texture.filename; + } + } +} + +graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, + image::TextureUsage::Type type, MapChannel channel) { + + if (baseUrl.isEmpty()) { + return nullptr; + } + + const auto url = getTextureUrl(baseUrl, hfmTexture); + const auto texture = DependencyManager::get()->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels); + _textures[channel] = Texture { hfmTexture.name, texture }; + + auto map = std::make_shared(); + if (texture) { + map->setTextureSource(texture->_textureSource); + } + map->setTextureTransform(hfmTexture.transform); + + return map; +} + +graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel) { + auto textureCache = DependencyManager::get(); + if (textureCache && !url.isEmpty()) { + auto texture = textureCache->getTexture(url, type); + _textures[channel].texture = texture; + + auto map = std::make_shared(); + if (texture) { + map->setTextureSource(texture->_textureSource); + } + + return map; + } + return nullptr; +} + +void NetworkMaterial::setAlbedoMap(const QUrl& url, bool useAlphaChannel) { + auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + if (map) { + map->setUseAlphaChannel(useAlphaChannel); + setTextureMap(MapChannel::ALBEDO_MAP, map); + } +} + +void NetworkMaterial::setNormalMap(const QUrl& url, bool isBumpmap) { + auto map = fetchTextureMap(url, isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); + if (map) { + setTextureMap(MapChannel::NORMAL_MAP, map); + } +} + +void NetworkMaterial::setRoughnessMap(const QUrl& url, bool isGloss) { + auto map = fetchTextureMap(url, isGloss ? image::TextureUsage::GLOSS_TEXTURE : image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + if (map) { + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } +} + +void NetworkMaterial::setMetallicMap(const QUrl& url, bool isSpecular) { + auto map = fetchTextureMap(url, isSpecular ? image::TextureUsage::SPECULAR_TEXTURE : image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + if (map) { + setTextureMap(MapChannel::METALLIC_MAP, map); + } +} + +void NetworkMaterial::setOcclusionMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + if (map) { + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } +} + +void NetworkMaterial::setEmissiveMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + if (map) { + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } +} + +void NetworkMaterial::setScatteringMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + if (map) { + setTextureMap(MapChannel::SCATTERING_MAP, map); + } +} + +void NetworkMaterial::setLightmapMap(const QUrl& url) { + auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + if (map) { + //map->setTextureTransform(_lightmapTransform); + //map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) : + graphics::Material(*material._material), + _textures(MapChannel::NUM_MAP_CHANNELS) +{ + _name = material.name.toStdString(); + if (!material.albedoTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + if (map) { + _albedoTransform = material.albedoTexture.transform; + map->setTextureTransform(_albedoTransform); + + if (!material.opacityTexture.filename.isEmpty()) { + if (material.albedoTexture.filename == material.opacityTexture.filename) { + // Best case scenario, just indicating that the albedo map contains transparency + // TODO: Different albedo/opacity maps are not currently supported + map->setUseAlphaChannel(true); + } + } + } + + setTextureMap(MapChannel::ALBEDO_MAP, map); + } + + + if (!material.normalTexture.filename.isEmpty()) { + auto type = (material.normalTexture.isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE); + auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP); + setTextureMap(MapChannel::NORMAL_MAP, map); + } + + if (!material.roughnessTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } else if (!material.glossTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, image::TextureUsage::GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } + + if (!material.metallicTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } else if (!material.specularTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, image::TextureUsage::SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } + + if (!material.occlusionTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + if (map) { + map->setTextureTransform(material.occlusionTexture.transform); + } + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } + + if (!material.emissiveTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } + + if (!material.scatteringTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.scatteringTexture, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + setTextureMap(MapChannel::SCATTERING_MAP, map); + } + + if (!material.lightmapTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + if (map) { + _lightmapTransform = material.lightmapTexture.transform; + _lightmapParams = material.lightmapParams; + map->setTextureTransform(_lightmapTransform); + map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + } + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +void NetworkMaterial::setTextures(const QVariantMap& textureMap) { + _isOriginal = false; + + const auto& albedoName = getTextureName(MapChannel::ALBEDO_MAP); + const auto& normalName = getTextureName(MapChannel::NORMAL_MAP); + const auto& roughnessName = getTextureName(MapChannel::ROUGHNESS_MAP); + const auto& metallicName = getTextureName(MapChannel::METALLIC_MAP); + const auto& occlusionName = getTextureName(MapChannel::OCCLUSION_MAP); + const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP); + const auto& lightmapName = getTextureName(MapChannel::LIGHTMAP_MAP); + const auto& scatteringName = getTextureName(MapChannel::SCATTERING_MAP); + + if (!albedoName.isEmpty()) { + auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); + if (map) { + map->setTextureTransform(_albedoTransform); + // when reassigning the albedo texture we also check for the alpha channel used as opacity + map->setUseAlphaChannel(true); + } + setTextureMap(MapChannel::ALBEDO_MAP, map); + } + + if (!normalName.isEmpty()) { + auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); + setTextureMap(MapChannel::NORMAL_MAP, map); + } + + if (!roughnessName.isEmpty()) { + auto url = textureMap.contains(roughnessName) ? textureMap[roughnessName].toUrl() : QUrl(); + // FIXME: If passing a gloss map instead of a roughmap how do we know? + auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } + + if (!metallicName.isEmpty()) { + auto url = textureMap.contains(metallicName) ? textureMap[metallicName].toUrl() : QUrl(); + // FIXME: If passing a specular map instead of a metallic how do we know? + auto map = fetchTextureMap(url, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } + + if (!occlusionName.isEmpty()) { + auto url = textureMap.contains(occlusionName) ? textureMap[occlusionName].toUrl() : QUrl(); + // FIXME: we need to handle the occlusion map transform here + auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } + + if (!emissiveName.isEmpty()) { + auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } + + if (!scatteringName.isEmpty()) { + auto url = textureMap.contains(scatteringName) ? textureMap[scatteringName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); + setTextureMap(MapChannel::SCATTERING_MAP, map); + } + + if (!lightmapName.isEmpty()) { + auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); + auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + if (map) { + map->setTextureTransform(_lightmapTransform); + map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); + } + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +bool NetworkMaterial::isMissingTexture() { + for (auto& networkTexture : _textures) { + auto& texture = networkTexture.texture; + if (!texture) { + continue; + } + // Failed texture downloads need to be considered as 'loaded' + // or the object will never fade in + bool finished = texture->isFailed() || (texture->isLoaded() && texture->getGPUTexture() && texture->getGPUTexture()->isDefined()); + if (!finished) { + return true; + } + } + return false; +} + +void NetworkMaterial::checkResetOpacityMap() { + // If material textures are loaded, check the material translucency + // FIXME: This should not be done here. The opacity map should already be reset in Material::setTextureMap. + // However, currently that code can be called before the albedo map is defined, so resetOpacityMap will fail. + // Geometry::areTexturesLoaded() is called repeatedly until it returns true, so we do the check here for now + const auto& albedoTexture = _textures[NetworkMaterial::MapChannel::ALBEDO_MAP]; + if (albedoTexture.texture) { + resetOpacityMap(); + } } \ No newline at end of file diff --git a/libraries/material-networking/src/material-networking/MaterialCache.h b/libraries/material-networking/src/material-networking/MaterialCache.h new file mode 100644 index 0000000000..4e6805ca39 --- /dev/null +++ b/libraries/material-networking/src/material-networking/MaterialCache.h @@ -0,0 +1,115 @@ +// +// Created by Sam Gondelman on 2/9/2018 +// Copyright 2018 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_MaterialCache_h +#define hifi_MaterialCache_h + +#include "glm/glm.hpp" + +#include +#include +#include + +#include "TextureCache.h" + +class NetworkMaterial : public graphics::Material { +public: + using MapChannel = graphics::Material::MapChannel; + + NetworkMaterial() : _textures(MapChannel::NUM_MAP_CHANNELS) {} + NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl); + NetworkMaterial(const NetworkMaterial& material); + + void setAlbedoMap(const QUrl& url, bool useAlphaChannel); + void setNormalMap(const QUrl& url, bool isBumpmap); + void setRoughnessMap(const QUrl& url, bool isGloss); + void setMetallicMap(const QUrl& url, bool isSpecular); + void setOcclusionMap(const QUrl& url); + void setEmissiveMap(const QUrl& url); + void setScatteringMap(const QUrl& url); + void setLightmapMap(const QUrl& url); + + bool isMissingTexture(); + void checkResetOpacityMap(); + +protected: + friend class Geometry; + + class Texture { + public: + QString name; + NetworkTexturePointer texture; + }; + using Textures = std::vector; + + Textures _textures; + + static const QString NO_TEXTURE; + const QString& getTextureName(MapChannel channel); + + void setTextures(const QVariantMap& textureMap); + + const bool& isOriginal() const { return _isOriginal; } + +private: + // Helpers for the ctors + QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture); + graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, + image::TextureUsage::Type type, MapChannel channel); + graphics::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel); + + Transform _albedoTransform; + Transform _lightmapTransform; + vec2 _lightmapParams; + + bool _isOriginal { true }; +}; + +class NetworkMaterialResource : public Resource { +public: + NetworkMaterialResource(const QUrl& url); + + QString getType() const override { return "NetworkMaterial"; } + + virtual void downloadFinished(const QByteArray& data) override; + + typedef struct ParsedMaterials { + uint version { 1 }; + std::vector names; + std::unordered_map> networkMaterials; + + void reset() { + version = 1; + names.clear(); + networkMaterials.clear(); + } + + } ParsedMaterials; + + ParsedMaterials parsedMaterials; + + static ParsedMaterials parseJSONMaterials(const QJsonDocument& materialJSON, const QUrl& baseUrl); + static std::pair> parseJSONMaterial(const QJsonObject& materialJSON, const QUrl& baseUrl); + +private: + static bool parseJSONColor(const QJsonValue& array, glm::vec3& color, bool& isSRGB); +}; + +using NetworkMaterialResourcePointer = QSharedPointer; + +class MaterialCache : public ResourceCache { +public: + static MaterialCache& instance(); + + NetworkMaterialResourcePointer getMaterial(const QUrl& url); + +protected: + virtual QSharedPointer createResource(const QUrl& url) override; + QSharedPointer createResourceCopy(const QSharedPointer& resource) override; +}; + +#endif diff --git a/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.cpp b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.cpp new file mode 100644 index 0000000000..9a99c21240 --- /dev/null +++ b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.cpp @@ -0,0 +1,11 @@ +// +// Created by Sam Gondelman on 2/7/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 "MaterialNetworkingLogging.h" + +Q_LOGGING_CATEGORY(materialnetworking, "hifi.gpu-material-network") diff --git a/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.h b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.h new file mode 100644 index 0000000000..a3f220d027 --- /dev/null +++ b/libraries/material-networking/src/material-networking/MaterialNetworkingLogging.h @@ -0,0 +1,11 @@ +// +// Created by Sam Gondelman on 2/7/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 + +Q_DECLARE_LOGGING_CATEGORY(materialnetworking) diff --git a/libraries/model-networking/src/model-networking/ShaderCache.cpp b/libraries/material-networking/src/material-networking/ShaderCache.cpp similarity index 100% rename from libraries/model-networking/src/model-networking/ShaderCache.cpp rename to libraries/material-networking/src/material-networking/ShaderCache.cpp diff --git a/libraries/model-networking/src/model-networking/ShaderCache.h b/libraries/material-networking/src/material-networking/ShaderCache.h similarity index 100% rename from libraries/model-networking/src/model-networking/ShaderCache.h rename to libraries/material-networking/src/material-networking/ShaderCache.h diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp similarity index 99% rename from libraries/model-networking/src/model-networking/TextureCache.cpp rename to libraries/material-networking/src/material-networking/TextureCache.cpp index 910de258f9..0e7d50a2e5 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -44,7 +44,7 @@ #include #include "NetworkLogging.h" -#include "ModelNetworkingLogging.h" +#include "MaterialNetworkingLogging.h" #include "NetworkingConstants.h" #include #include @@ -938,7 +938,7 @@ void NetworkTexture::handleFinishedInitialLoad() { cache::FilePointer file; auto& ktxCache = textureCache->_ktxCache; if (!memKtx || !(file = ktxCache->writeFile(data, KTXCache::Metadata(filename, length)))) { - qCWarning(modelnetworking) << url << " failed to write cache file"; + qCWarning(materialnetworking) << url << " failed to write cache file"; QMetaObject::invokeMethod(resource.data(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), @@ -1126,7 +1126,7 @@ void ImageReader::listSupportedImageFormats() { static std::once_flag once; std::call_once(once, []{ auto supportedFormats = QImageReader::supportedImageFormats(); - qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); + qCDebug(materialnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); }); } @@ -1174,7 +1174,7 @@ void ImageReader::read() { if (texture) { texture = textureCache->cacheTextureByHash(hash, texture); } else { - qCWarning(modelnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating..."; + qCWarning(materialnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating..."; } } } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/material-networking/src/material-networking/TextureCache.h similarity index 100% rename from libraries/model-networking/src/model-networking/TextureCache.h rename to libraries/material-networking/src/material-networking/TextureCache.h diff --git a/libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.cpp b/libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.cpp similarity index 100% rename from libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.cpp rename to libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.cpp diff --git a/libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.h b/libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.h similarity index 100% rename from libraries/model-networking/src/model-networking/TextureCacheScriptingInterface.h rename to libraries/material-networking/src/material-networking/TextureCacheScriptingInterface.h diff --git a/libraries/model-baker/CMakeLists.txt b/libraries/model-baker/CMakeLists.txt index 6fa7c1815a..1e67d04bc1 100644 --- a/libraries/model-baker/CMakeLists.txt +++ b/libraries/model-baker/CMakeLists.txt @@ -1,4 +1,5 @@ set(TARGET_NAME model-baker) setup_hifi_library() -link_hifi_libraries(shared task gpu graphics hfm) +link_hifi_libraries(shared shaders task gpu graphics hfm material-networking) +include_hifi_library_headers(networking) \ No newline at end of file diff --git a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp index c9b4fec1e1..37ea184360 100644 --- a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp @@ -10,6 +10,8 @@ #include "ModelBakerLogging.h" +#include + void ApplyMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& materialsIn = input.get0(); const auto& mapping = input.get1(); diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index 6a7182cc33..7701f54d43 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,6 +1,5 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared shaders networking graphics fbx ktx image gl model-baker) -include_hifi_library_headers(gpu) +link_hifi_libraries(shared shaders networking graphics fbx material-networking model-baker) include_hifi_library_headers(hfm) include_hifi_library_headers(task) diff --git a/libraries/model-networking/src/model-networking/MaterialCache.h b/libraries/model-networking/src/model-networking/MaterialCache.h deleted file mode 100644 index 6abadfc030..0000000000 --- a/libraries/model-networking/src/model-networking/MaterialCache.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// Created by Sam Gondelman on 2/9/2018 -// Copyright 2018 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_MaterialCache_h -#define hifi_MaterialCache_h - -#include - -#include "glm/glm.hpp" - -#include "ModelCache.h" - -class NetworkMaterialResource : public Resource { -public: - NetworkMaterialResource(const QUrl& url); - - QString getType() const override { return "NetworkMaterial"; } - - virtual void downloadFinished(const QByteArray& data) override; - - typedef struct ParsedMaterials { - uint version { 1 }; - std::vector names; - std::unordered_map> networkMaterials; - - void reset() { - version = 1; - names.clear(); - networkMaterials.clear(); - } - - } ParsedMaterials; - - ParsedMaterials parsedMaterials; - - static ParsedMaterials parseJSONMaterials(const QJsonDocument& materialJSON, const QUrl& baseUrl); - static std::pair> parseJSONMaterial(const QJsonObject& materialJSON, const QUrl& baseUrl); - -private: - static bool parseJSONColor(const QJsonValue& array, glm::vec3& color, bool& isSRGB); -}; - -using NetworkMaterialResourcePointer = QSharedPointer; - -class MaterialCache : public ResourceCache { -public: - static MaterialCache& instance(); - - NetworkMaterialResourcePointer getMaterial(const QUrl& url); - -protected: - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; -}; - -#endif diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index a4eba0c7a9..20ca97a681 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -556,310 +556,4 @@ void GeometryResourceWatcher::resourceRefreshed() { // _instance.reset(); } -NetworkMaterial::NetworkMaterial(const NetworkMaterial& m) : - Material(m), - _textures(m._textures), - _albedoTransform(m._albedoTransform), - _lightmapTransform(m._lightmapTransform), - _lightmapParams(m._lightmapParams), - _isOriginal(m._isOriginal) -{} - -const QString NetworkMaterial::NO_TEXTURE = QString(); - -const QString& NetworkMaterial::getTextureName(MapChannel channel) { - if (_textures[channel].texture) { - return _textures[channel].name; - } - return NO_TEXTURE; -} - -QUrl NetworkMaterial::getTextureUrl(const QUrl& baseUrl, const HFMTexture& texture) { - if (texture.content.isEmpty()) { - // External file: search relative to the baseUrl, in case filename is relative - return baseUrl.resolved(QUrl(texture.filename)); - } else { - // Inlined file: cache under the fbx file to avoid namespace clashes - // NOTE: We cannot resolve the path because filename may be an absolute path - assert(texture.filename.size() > 0); - auto baseUrlStripped = baseUrl.toDisplayString(QUrl::RemoveFragment | QUrl::RemoveQuery | QUrl::RemoveUserInfo); - if (texture.filename.at(0) == '/') { - return baseUrlStripped + texture.filename; - } else { - return baseUrlStripped + '/' + texture.filename; - } - } -} - -graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, - image::TextureUsage::Type type, MapChannel channel) { - - if (baseUrl.isEmpty()) { - return nullptr; - } - - const auto url = getTextureUrl(baseUrl, hfmTexture); - const auto texture = DependencyManager::get()->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels); - _textures[channel] = Texture { hfmTexture.name, texture }; - - auto map = std::make_shared(); - if (texture) { - map->setTextureSource(texture->_textureSource); - } - map->setTextureTransform(hfmTexture.transform); - - return map; -} - -graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel) { - auto textureCache = DependencyManager::get(); - if (textureCache && !url.isEmpty()) { - auto texture = textureCache->getTexture(url, type); - _textures[channel].texture = texture; - - auto map = std::make_shared(); - if (texture) { - map->setTextureSource(texture->_textureSource); - } - - return map; - } - return nullptr; -} - -void NetworkMaterial::setAlbedoMap(const QUrl& url, bool useAlphaChannel) { - auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - if (map) { - map->setUseAlphaChannel(useAlphaChannel); - setTextureMap(MapChannel::ALBEDO_MAP, map); - } -} - -void NetworkMaterial::setNormalMap(const QUrl& url, bool isBumpmap) { - auto map = fetchTextureMap(url, isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); - if (map) { - setTextureMap(MapChannel::NORMAL_MAP, map); - } -} - -void NetworkMaterial::setRoughnessMap(const QUrl& url, bool isGloss) { - auto map = fetchTextureMap(url, isGloss ? image::TextureUsage::GLOSS_TEXTURE : image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); - if (map) { - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } -} - -void NetworkMaterial::setMetallicMap(const QUrl& url, bool isSpecular) { - auto map = fetchTextureMap(url, isSpecular ? image::TextureUsage::SPECULAR_TEXTURE : image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); - if (map) { - setTextureMap(MapChannel::METALLIC_MAP, map); - } -} - -void NetworkMaterial::setOcclusionMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); - if (map) { - setTextureMap(MapChannel::OCCLUSION_MAP, map); - } -} - -void NetworkMaterial::setEmissiveMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); - if (map) { - setTextureMap(MapChannel::EMISSIVE_MAP, map); - } -} - -void NetworkMaterial::setScatteringMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); - if (map) { - setTextureMap(MapChannel::SCATTERING_MAP, map); - } -} - -void NetworkMaterial::setLightmapMap(const QUrl& url) { - auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - if (map) { - //map->setTextureTransform(_lightmapTransform); - //map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - setTextureMap(MapChannel::LIGHTMAP_MAP, map); - } -} - -NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) : - graphics::Material(*material._material), - _textures(MapChannel::NUM_MAP_CHANNELS) -{ - _name = material.name.toStdString(); - if (!material.albedoTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - if (map) { - _albedoTransform = material.albedoTexture.transform; - map->setTextureTransform(_albedoTransform); - - if (!material.opacityTexture.filename.isEmpty()) { - if (material.albedoTexture.filename == material.opacityTexture.filename) { - // Best case scenario, just indicating that the albedo map contains transparency - // TODO: Different albedo/opacity maps are not currently supported - map->setUseAlphaChannel(true); - } - } - } - - setTextureMap(MapChannel::ALBEDO_MAP, map); - } - - - if (!material.normalTexture.filename.isEmpty()) { - auto type = (material.normalTexture.isBumpmap ? image::TextureUsage::BUMP_TEXTURE : image::TextureUsage::NORMAL_TEXTURE); - auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP); - setTextureMap(MapChannel::NORMAL_MAP, map); - } - - if (!material.roughnessTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } else if (!material.glossTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, image::TextureUsage::GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } - - if (!material.metallicTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); - setTextureMap(MapChannel::METALLIC_MAP, map); - } else if (!material.specularTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, image::TextureUsage::SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); - setTextureMap(MapChannel::METALLIC_MAP, map); - } - - if (!material.occlusionTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); - if (map) { - map->setTextureTransform(material.occlusionTexture.transform); - } - setTextureMap(MapChannel::OCCLUSION_MAP, map); - } - - if (!material.emissiveTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); - setTextureMap(MapChannel::EMISSIVE_MAP, map); - } - - if (!material.scatteringTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.scatteringTexture, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); - setTextureMap(MapChannel::SCATTERING_MAP, map); - } - - if (!material.lightmapTexture.filename.isEmpty()) { - auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - if (map) { - _lightmapTransform = material.lightmapTexture.transform; - _lightmapParams = material.lightmapParams; - map->setTextureTransform(_lightmapTransform); - map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - } - setTextureMap(MapChannel::LIGHTMAP_MAP, map); - } -} - -void NetworkMaterial::setTextures(const QVariantMap& textureMap) { - _isOriginal = false; - - const auto& albedoName = getTextureName(MapChannel::ALBEDO_MAP); - const auto& normalName = getTextureName(MapChannel::NORMAL_MAP); - const auto& roughnessName = getTextureName(MapChannel::ROUGHNESS_MAP); - const auto& metallicName = getTextureName(MapChannel::METALLIC_MAP); - const auto& occlusionName = getTextureName(MapChannel::OCCLUSION_MAP); - const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP); - const auto& lightmapName = getTextureName(MapChannel::LIGHTMAP_MAP); - const auto& scatteringName = getTextureName(MapChannel::SCATTERING_MAP); - - if (!albedoName.isEmpty()) { - auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP); - if (map) { - map->setTextureTransform(_albedoTransform); - // when reassigning the albedo texture we also check for the alpha channel used as opacity - map->setUseAlphaChannel(true); - } - setTextureMap(MapChannel::ALBEDO_MAP, map); - } - - if (!normalName.isEmpty()) { - auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP); - setTextureMap(MapChannel::NORMAL_MAP, map); - } - - if (!roughnessName.isEmpty()) { - auto url = textureMap.contains(roughnessName) ? textureMap[roughnessName].toUrl() : QUrl(); - // FIXME: If passing a gloss map instead of a roughmap how do we know? - auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); - setTextureMap(MapChannel::ROUGHNESS_MAP, map); - } - - if (!metallicName.isEmpty()) { - auto url = textureMap.contains(metallicName) ? textureMap[metallicName].toUrl() : QUrl(); - // FIXME: If passing a specular map instead of a metallic how do we know? - auto map = fetchTextureMap(url, image::TextureUsage::METALLIC_TEXTURE, MapChannel::METALLIC_MAP); - setTextureMap(MapChannel::METALLIC_MAP, map); - } - - if (!occlusionName.isEmpty()) { - auto url = textureMap.contains(occlusionName) ? textureMap[occlusionName].toUrl() : QUrl(); - // FIXME: we need to handle the occlusion map transform here - auto map = fetchTextureMap(url, image::TextureUsage::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); - setTextureMap(MapChannel::OCCLUSION_MAP, map); - } - - if (!emissiveName.isEmpty()) { - auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); - setTextureMap(MapChannel::EMISSIVE_MAP, map); - } - - if (!scatteringName.isEmpty()) { - auto url = textureMap.contains(scatteringName) ? textureMap[scatteringName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::SCATTERING_TEXTURE, MapChannel::SCATTERING_MAP); - setTextureMap(MapChannel::SCATTERING_MAP, map); - } - - if (!lightmapName.isEmpty()) { - auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl(); - auto map = fetchTextureMap(url, image::TextureUsage::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); - if (map) { - map->setTextureTransform(_lightmapTransform); - map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y); - } - setTextureMap(MapChannel::LIGHTMAP_MAP, map); - } -} - -bool NetworkMaterial::isMissingTexture() { - for (auto& networkTexture : _textures) { - auto& texture = networkTexture.texture; - if (!texture) { - continue; - } - // Failed texture downloads need to be considered as 'loaded' - // or the object will never fade in - bool finished = texture->isFailed() || (texture->isLoaded() && texture->getGPUTexture() && texture->getGPUTexture()->isDefined()); - if (!finished) { - return true; - } - } - return false; -} - -void NetworkMaterial::checkResetOpacityMap() { - // If material textures are loaded, check the material translucency - // FIXME: This should not be done here. The opacity map should already be reset in Material::setTextureMap. - // However, currently that code can be called before the albedo map is defined, so resetOpacityMap will fail. - // Geometry::areTexturesLoaded() is called repeatedly until it returns true, so we do the check here for now - const auto& albedoTexture = _textures[NetworkMaterial::MapChannel::ALBEDO_MAP]; - if (albedoTexture.texture) { - resetOpacityMap(); - } -} - #include "ModelCache.moc" diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 497cae86a3..59fd6a4b74 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -15,17 +15,13 @@ #include #include -#include #include #include "FBXSerializer.h" -#include "TextureCache.h" +#include +#include #include "ModelLoader.h" -// Alias instead of derive to avoid copying - -class NetworkTexture; -class NetworkMaterial; class MeshPart; class GeometryMappingResource; @@ -166,59 +162,6 @@ private: ModelLoader _modelLoader; }; -class NetworkMaterial : public graphics::Material { -public: - using MapChannel = graphics::Material::MapChannel; - - NetworkMaterial() : _textures(MapChannel::NUM_MAP_CHANNELS) {} - NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl); - NetworkMaterial(const NetworkMaterial& material); - - void setAlbedoMap(const QUrl& url, bool useAlphaChannel); - void setNormalMap(const QUrl& url, bool isBumpmap); - void setRoughnessMap(const QUrl& url, bool isGloss); - void setMetallicMap(const QUrl& url, bool isSpecular); - void setOcclusionMap(const QUrl& url); - void setEmissiveMap(const QUrl& url); - void setScatteringMap(const QUrl& url); - void setLightmapMap(const QUrl& url); - - bool isMissingTexture(); - void checkResetOpacityMap(); - -protected: - friend class Geometry; - - class Texture { - public: - QString name; - NetworkTexturePointer texture; - }; - using Textures = std::vector; - - Textures _textures; - - static const QString NO_TEXTURE; - const QString& getTextureName(MapChannel channel); - - void setTextures(const QVariantMap& textureMap); - - const bool& isOriginal() const { return _isOriginal; } - -private: - // Helpers for the ctors - QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture); - graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture, - image::TextureUsage::Type type, MapChannel channel); - graphics::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel); - - Transform _albedoTransform; - Transform _lightmapTransform; - vec2 _lightmapParams; - - bool _isOriginal { true }; -}; - class MeshPart { public: MeshPart(int mesh, int part, int material) : meshID { mesh }, partID { part }, materialID { material } {} diff --git a/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp index 0c44fa33eb..d76efec31a 100644 --- a/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp +++ b/libraries/model-networking/src/model-networking/ModelNetworkingLogging.cpp @@ -8,4 +8,4 @@ #include "ModelNetworkingLogging.h" -Q_LOGGING_CATEGORY(modelnetworking, "hifi.gpu-network") +Q_LOGGING_CATEGORY(modelnetworking, "hifi.gpu-model-network") diff --git a/libraries/physics/CMakeLists.txt b/libraries/physics/CMakeLists.txt index 5249ed2950..d7ce40641d 100644 --- a/libraries/physics/CMakeLists.txt +++ b/libraries/physics/CMakeLists.txt @@ -7,6 +7,7 @@ include_hifi_library_headers(avatars) include_hifi_library_headers(audio) include_hifi_library_headers(octree) include_hifi_library_headers(animation) +include_hifi_library_headers(material-networking) include_hifi_library_headers(model-networking) include_hifi_library_headers(image) include_hifi_library_headers(ktx) diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt index 6d6610a323..06d2a3d1ed 100644 --- a/libraries/procedural/CMakeLists.txt +++ b/libraries/procedural/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME procedural) setup_hifi_library() -link_hifi_libraries(shared gpu shaders networking graphics model-networking ktx image) +link_hifi_libraries(shared gpu shaders networking graphics material-networking ktx image) diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 3e10678ba7..8477e69afc 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -19,8 +19,8 @@ #include #include #include -#include -#include +#include +#include using UniformLambdas = std::list>; const size_t MAX_PROCEDURAL_TEXTURE_CHANNELS{ 4 }; diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 2b092bff2a..833d78bb74 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ set(TARGET_NAME render-utils) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Gui Network Qml Quick Script) -link_hifi_libraries(shared task ktx gpu shaders graphics graphics-scripting model-networking render animation fbx image procedural) +link_hifi_libraries(shared task ktx gpu shaders graphics graphics-scripting material-networking model-networking render animation fbx image procedural) include_hifi_library_headers(audio) include_hifi_library_headers(networking) include_hifi_library_headers(octree) diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 5f3763ac2a..6f6f2ab856 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -15,7 +15,7 @@ #include #include -#include +#include #include #include #include diff --git a/libraries/render-utils/src/TextureCache.h b/libraries/render-utils/src/TextureCache.h index d6c1e419b9..f010666e19 100644 --- a/libraries/render-utils/src/TextureCache.h +++ b/libraries/render-utils/src/TextureCache.h @@ -1,2 +1,2 @@ // Compatibility -#include +#include diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 346c6e50f6..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 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 includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit include_hifi_library_headers(gl) diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 8e43397c19..dcb2e39e1b 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -11,7 +11,7 @@ if (WIN32 AND (NOT USE_GLES)) setup_hifi_plugin(Gui Qml Multimedia) link_hifi_libraries(shared task gl qml networking controllers ui plugins display-plugins ui-plugins input-plugins script-engine - audio-client render-utils graphics shaders gpu render model-networking model-baker hfm fbx ktx image procedural ${PLATFORM_GL_BACKEND}) + audio-client render-utils graphics shaders gpu render material-networking model-networking model-baker hfm fbx ktx image procedural ${PLATFORM_GL_BACKEND}) include_hifi_library_headers(octree) target_openvr() From 4f03157f3989afb4d337bc951f1a2c8dfd70af19 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 11 Feb 2019 10:20:55 -0800 Subject: [PATCH 05/43] working on fst material mapping --- libraries/material-networking/CMakeLists.txt | 2 +- .../src/material-networking/MaterialCache.h | 2 + .../model-baker/ApplyMaterialMappingTask.cpp | 57 ----------------- .../model-baker/src/model-baker/Baker.cpp | 23 +++---- libraries/model-baker/src/model-baker/Baker.h | 3 + .../model-baker/ParseMaterialMappingTask.cpp | 63 +++++++++++++++++++ ...ppingTask.h => ParseMaterialMappingTask.h} | 14 +++-- .../src/model-networking/ModelCache.cpp | 3 + .../src/model-networking/ModelCache.h | 2 + libraries/networking/src/ResourceCache.h | 2 +- .../render-utils/src/CauterizedModel.cpp | 1 + libraries/render-utils/src/Model.cpp | 50 ++++++++++++++- libraries/render-utils/src/Model.h | 3 + 13 files changed, 146 insertions(+), 79 deletions(-) delete mode 100644 libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp create mode 100644 libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp rename libraries/model-baker/src/model-baker/{ApplyMaterialMappingTask.h => ParseMaterialMappingTask.h} (62%) diff --git a/libraries/material-networking/CMakeLists.txt b/libraries/material-networking/CMakeLists.txt index 4ade61230a..2bf8ea213d 100644 --- a/libraries/material-networking/CMakeLists.txt +++ b/libraries/material-networking/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME material-networking) setup_hifi_library() -link_hifi_libraries(shared shaders networking graphics fbx ktx image gl) +link_hifi_libraries(shared shaders networking graphics ktx image gl) include_hifi_library_headers(gpu) include_hifi_library_headers(hfm) \ No newline at end of file diff --git a/libraries/material-networking/src/material-networking/MaterialCache.h b/libraries/material-networking/src/material-networking/MaterialCache.h index 4e6805ca39..d327aedb22 100644 --- a/libraries/material-networking/src/material-networking/MaterialCache.h +++ b/libraries/material-networking/src/material-networking/MaterialCache.h @@ -71,6 +71,7 @@ private: class NetworkMaterialResource : public Resource { public: + NetworkMaterialResource() : Resource() {} NetworkMaterialResource(const QUrl& url); QString getType() const override { return "NetworkMaterial"; } @@ -100,6 +101,7 @@ private: }; using NetworkMaterialResourcePointer = QSharedPointer; +using MaterialMapping = std::vector>; class MaterialCache : public ResourceCache { public: diff --git a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp deleted file mode 100644 index 37ea184360..0000000000 --- a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// -// Created by Sam Gondelman on 2/7/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 "ApplyMaterialMappingTask.h" - -#include "ModelBakerLogging.h" - -#include - -void ApplyMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { - const auto& materialsIn = input.get0(); - const auto& mapping = input.get1(); - - auto materialsOut = materialsIn; - - auto mappingIter = mapping.find("materialMap"); - if (mappingIter != mapping.end()) { - QByteArray materialMapValue = mappingIter.value().toByteArray(); - QJsonObject materialMap = QJsonDocument::fromJson(materialMapValue).object(); - if (materialMap.isEmpty()) { - qCDebug(model_baker) << "Material Map found but did not produce valid JSON:" << materialMapValue; - } else { - for (auto& material : materialsOut) { - auto materialMapIter = materialMap.find(material.name); - if (materialMapIter != materialMap.end()) { - QJsonObject materialOptions = materialMapIter.value().toObject(); - qCDebug(model_baker) << "Mapping material:" << material.name << " with HFMaterial: " << materialOptions; - - { - auto scatteringIter = materialOptions.find("scattering"); - if (scatteringIter != materialOptions.end()) { - float scattering = (float)scatteringIter.value().toDouble(); - material._material->setScattering(scattering); - } - } - - { - auto scatteringMapIter = materialOptions.find("scatteringMap"); - if (scatteringMapIter != materialOptions.end()) { - QByteArray scatteringMap = scatteringMapIter.value().toVariant().toByteArray(); - material.scatteringTexture = HFMTexture(); - material.scatteringTexture.name = material.name + ".scatteringMap"; - material.scatteringTexture.filename = scatteringMap; - } - } - } - } - } - } - - output = materialsOut; -} diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 981d799f12..037cfb7912 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -20,7 +20,6 @@ #include "CalculateBlendshapeNormalsTask.h" #include "CalculateBlendshapeTangentsTask.h" #include "PrepareJointsTask.h" -#include "ApplyMaterialMappingTask.h" namespace baker { @@ -102,7 +101,7 @@ namespace baker { class BuildModelTask { public: - using Input = VaryingSet6, std::vector, QMap, QHash, QHash>; + using Input = VaryingSet5, std::vector, QMap, QHash>; using Output = hfm::Model::Pointer; using JobModel = Job::ModelIO; @@ -112,7 +111,6 @@ namespace baker { hfmModelOut->joints = QVector::fromStdVector(input.get2()); hfmModelOut->jointRotationOffsets = input.get3(); hfmModelOut->jointIndices = input.get4(); - hfmModelOut->materials = input.get5(); output = hfmModelOut; } }; @@ -120,9 +118,9 @@ namespace baker { class BakerEngineBuilder { public: using Input = VaryingSet2; - using Output = hfm::Model::Pointer; + using Output = VaryingSet2; using JobModel = Task::ModelIO; - void build(JobModel& model, const Varying& input, Varying& hfmModelOut) { + void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); const auto& mapping = input.getN(1); @@ -156,17 +154,19 @@ namespace baker { const auto jointRotationOffsets = jointInfoOut.getN(1); const auto jointIndices = jointInfoOut.getN(2); - // Apply material mapping - const auto materialMappingInputs = ApplyMaterialMappingTask::Input(materials, mapping).asVarying(); - const auto materialsOut = model.addJob("ApplyMaterialMapping", materialMappingInputs); + // Parse material mapping + const auto materialMappingInputs = ParseMaterialMappingTask::Input(materials, mapping).asVarying(); + const auto materialMapping = model.addJob("ParseMaterialMapping", materialMappingInputs); // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs); const auto buildMeshesInputs = BuildMeshesTask::Input(meshesIn, graphicsMeshes, normalsPerMesh, tangentsPerMesh, blendshapesPerMeshOut).asVarying(); const auto meshesOut = model.addJob("BuildMeshes", buildMeshesInputs); - const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, materialsOut).asVarying(); - hfmModelOut = model.addJob("BuildModel", buildModelInputs); + const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying(); + const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); + + output = Output(hfmModelOut, materialMapping); } }; @@ -178,7 +178,8 @@ namespace baker { void Baker::run() { _engine->run(); - hfmModel = _engine->getOutput().get(); + hfmModel = _engine->getOutput().get().get0(); + materialMapping = _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 41989d73df..542be0b559 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -18,6 +18,8 @@ #include "Engine.h" +#include "ParseMaterialMappingTask.h" + namespace baker { class Baker { public: @@ -27,6 +29,7 @@ namespace baker { // Outputs, available after run() is called hfm::Model::Pointer hfmModel; + MaterialMapping materialMapping; protected: EnginePointer _engine; diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp new file mode 100644 index 0000000000..7a4acb595a --- /dev/null +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -0,0 +1,63 @@ +// +// Created by Sam Gondelman on 2/7/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 "ParseMaterialMappingTask.h" + +#include "ModelBakerLogging.h" + +void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { + const auto& materialsIn = input.get0(); + const auto& mapping = input.get1(); + + MaterialMapping materialMapping; + + auto mappingIter = mapping.find("materialMap"); + if (mappingIter != mapping.end()) { + QByteArray materialMapValue = mappingIter.value().toByteArray(); + QJsonObject materialMap = QJsonDocument::fromJson(materialMapValue).object(); + if (materialMap.isEmpty()) { + qCDebug(model_baker) << "Material Map found but did not produce valid JSON:" << materialMapValue; + } else { + auto mappingKeys = materialMap.keys(); + for (auto mapping : mappingKeys) { + auto mappingValue = materialMap[mapping].toObject(); + + // Old subsurface scattering mapping + { + auto scatteringIter = mappingValue.find("scattering"); + auto scatteringMapIter = mappingValue.find("scatteringMap"); + if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) { + std::shared_ptr material = std::make_shared(); + + if (scatteringIter != mappingValue.end()) { + float scattering = (float)scatteringIter.value().toDouble(); + material->setScattering(scattering); + } + + if (scatteringMapIter != mappingValue.end()) { + QString scatteringMap = scatteringMapIter.value().toString(); + material->setScatteringMap(scatteringMap); + } + + material->setDefaultFallthrough(true); + + NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); }); + materialResource->moveToThread(qApp->thread()); + materialResource->parsedMaterials.names.push_back("scattering"); + materialResource->parsedMaterials.networkMaterials["scattering"] = material; + + materialMapping.push_back(std::pair("mat::" + mapping.toStdString(), materialResource)); + continue; + } + } + } + } + } + + output = materialMapping; +} diff --git a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h similarity index 62% rename from libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.h rename to libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h index 271c80fe67..3e48c00acf 100644 --- a/libraries/model-baker/src/model-baker/ApplyMaterialMappingTask.h +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -6,8 +6,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_ApplyMaterialMappingTask_h -#define hifi_ApplyMaterialMappingTask_h +#ifndef hifi_ParseMaterialMappingTask_h +#define hifi_ParseMaterialMappingTask_h #include @@ -15,13 +15,15 @@ #include "Engine.h" -class ApplyMaterialMappingTask { +#include + +class ParseMaterialMappingTask { public: using Input = baker::VaryingSet2, QVariantHash>; - using Output = QHash; - using JobModel = baker::Job::ModelIO; + using Output = MaterialMapping; + using JobModel = baker::Job::ModelIO; void run(const baker::BakeContextPointer& context, const Input& input, Output& output); }; -#endif // hifi_ApplyMaterialMappingTask_h \ No newline at end of file +#endif // hifi_ParseMaterialMappingTask_h \ No newline at end of file diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 20ca97a681..0ed518937d 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -174,6 +174,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) { void GeometryMappingResource::onGeometryMappingLoaded(bool success) { if (success && _geometryResource) { _hfmModel = _geometryResource->_hfmModel; + _materialMapping = _geometryResource->_materialMapping; _meshParts = _geometryResource->_meshParts; _meshes = _geometryResource->_meshes; _materials = _geometryResource->_materials; @@ -341,6 +342,7 @@ void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmMode // Assume ownership of the processed HFMModel _hfmModel = modelBaker.hfmModel; + _materialMapping = modelBaker.materialMapping; // Copy materials QHash materialIDAtlas; @@ -437,6 +439,7 @@ const QVariantMap Geometry::getTextures() const { // FIXME: The materials should only be copied when modified, but the Model currently caches the original Geometry::Geometry(const Geometry& geometry) { _hfmModel = geometry._hfmModel; + _materialMapping = geometry._materialMapping; _meshes = geometry._meshes; _meshParts = geometry._meshParts; diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 59fd6a4b74..4cd7048dca 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -45,6 +45,7 @@ public: bool isHFMModelLoaded() const { return (bool)_hfmModel; } const HFMModel& getHFMModel() const { return *_hfmModel; } + const MaterialMapping& getMaterialMapping() const { return _materialMapping; } const GeometryMeshes& getMeshes() const { return *_meshes; } const std::shared_ptr getShapeMaterial(int shapeID) const; @@ -60,6 +61,7 @@ protected: // Shared across all geometries, constant throughout lifetime std::shared_ptr _hfmModel; + MaterialMapping _materialMapping; std::shared_ptr _meshes; std::shared_ptr _meshParts; diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 740bdadc48..4693bc0613 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -359,7 +359,7 @@ class Resource : public QObject { Q_OBJECT public: - + Resource() : QObject(), _loaded(true) {} Resource(const Resource& other); Resource(const QUrl& url); virtual ~Resource(); diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 81a81c5602..691dae0339 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -97,6 +97,7 @@ void CauterizedModel::createRenderItemSet() { } } _blendshapeOffsetsInitialized = true; + applyMaterialMapping(); } else { Model::createRenderItemSet(); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 0206bd6963..722f12eceb 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1465,6 +1465,7 @@ void Model::createRenderItemSet() { } } _blendshapeOffsetsInitialized = true; + applyMaterialMapping(); } bool Model::isRenderable() const { @@ -1519,17 +1520,60 @@ std::set Model::getMeshIDsFromMaterialID(QString parentMaterialNam return toReturn; } +void Model::applyMaterialMapping() { + auto renderItemsKey = _renderItemKeyGlobalFlags; + PrimitiveMode primitiveMode = getPrimitiveMode(); + bool useDualQuaternionSkinning = _useDualQuaternionSkinning; + + render::Transaction transaction; + std::unordered_map priorityMap; + auto& materialMapping = getMaterialMapping(); + qDebug() << "boop" << materialMapping.size(); + for (auto& mapping : materialMapping) { + std::set shapeIDs = getMeshIDsFromMaterialID(QString(mapping.first.c_str())); + + qDebug() << "boop2" << mapping.first.c_str() << shapeIDs.size(); + if (shapeIDs.size() == 0) { + continue; + } + + auto networkMaterialResource = mapping.second; + qDebug() << (bool)networkMaterialResource; + if (networkMaterialResource && networkMaterialResource->isLoaded() && networkMaterialResource->parsedMaterials.names.size() > 0) { + auto networkMaterial = networkMaterialResource->parsedMaterials.networkMaterials[networkMaterialResource->parsedMaterials.names[0]]; + for (auto shapeID : shapeIDs) { + if (shapeID < _modelMeshRenderItemIDs.size()) { + auto itemID = _modelMeshRenderItemIDs[shapeID]; + auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; + bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); + graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, ++priorityMap[shapeID]); + transaction.updateItem(itemID, [material, renderItemsKey, + invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { + data.addMaterial(material); + // if the material changed, we might need to update our item key or shape key + data.updateKey(renderItemsKey); + data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning); + }); + } + } + } + } + AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); +} + void Model::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { std::set shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str())); + + auto renderItemsKey = _renderItemKeyGlobalFlags; + PrimitiveMode primitiveMode = getPrimitiveMode(); + bool useDualQuaternionSkinning = _useDualQuaternionSkinning; + render::Transaction transaction; for (auto shapeID : shapeIDs) { if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; - auto renderItemsKey = _renderItemKeyGlobalFlags; - PrimitiveMode primitiveMode = getPrimitiveMode(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); - bool useDualQuaternionSkinning = _useDualQuaternionSkinning; transaction.updateItem(itemID, [material, renderItemsKey, invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.addMaterial(material); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index aadfca78ba..672a4c61eb 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -183,6 +183,7 @@ public: /// Provided as a convenience, will crash if !isLoaded() // And so that getHFMModel() isn't chained everywhere const HFMModel& getHFMModel() const { assert(isLoaded()); return _renderGeometry->getHFMModel(); } + const MaterialMapping& getMaterialMapping() const { assert(isLoaded()); return _renderGeometry->getMaterialMapping(); } bool isActive() const { return isLoaded(); } @@ -373,6 +374,8 @@ signals: protected: + void applyMaterialMapping(); + void setBlendshapeCoefficients(const QVector& coefficients) { _blendshapeCoefficients = coefficients; } const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } From a0199c58841cbeee79ef5c9b8b176b61d43e133a Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 11 Feb 2019 12:27:02 -0800 Subject: [PATCH 06/43] Case 21119, 21120 Case 21119 - text filter doesn't change as you type in new (qml) marketplace Case 21120 - text filter doesn't clear in new (qml) marketplace --- .../hifi/commerce/marketplace/Marketplace.qml | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 0d42cb599e..0f02e46529 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -316,23 +316,28 @@ Rectangle { font.pixelSize: hifi.fontSizes.textFieldInput placeholderText: "Search Marketplace" + Timer { + id: keypressTimer + running: false + repeat: false + interval: 300 + onTriggered: searchField.accepted() + + } + // workaround for https://bugreports.qt.io/browse/QTBUG-49297 Keys.onPressed: { switch (event.key) { case Qt.Key_Return: case Qt.Key_Enter: event.accepted = true; + searchField.text = ""; - // emit accepted signal manually - if (acceptableInput) { - searchField.accepted(); - searchField.forceActiveFocus(); - } + getMarketplaceItems(); + searchField.forceActiveFocus(); break; - case Qt.Key_Backspace: - if (searchField.text === "") { - primaryFilter_index = -1; - } + default: + keypressTimer.restart(); break; } } From 4f8de7ed0bd912fda1a3124103a8b0718ab42509 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 11 Feb 2019 12:48:04 -0800 Subject: [PATCH 07/43] Case 21123, Case - marketplace license display has weird crud in it (new qml version) --- .../qml/hifi/commerce/marketplace/Marketplace.qml | 2 ++ .../hifi/commerce/marketplace/MarketplaceItem.qml | 2 +- .../qml/hifi/commerce/marketplace/licenses/Popv1.txt | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 interface/resources/qml/hifi/commerce/marketplace/licenses/Popv1.txt diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 0f02e46529..65fe35d4ee 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -1130,6 +1130,8 @@ Rectangle { fill: parent } + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + RalewayRegular { id: licenseText diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 0a57e56099..2c7a50033c 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -520,7 +520,7 @@ Rectangle { } else if (root.license === "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)") { url = "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.txt" } else if (root.license === "Proof of Provenance License (PoP License)") { - url = "https://digitalassetregistry.com/PoP-License/v1/" + url = "licenses/Popv1.txt" } if(url) { showLicense(url) diff --git a/interface/resources/qml/hifi/commerce/marketplace/licenses/Popv1.txt b/interface/resources/qml/hifi/commerce/marketplace/licenses/Popv1.txt new file mode 100644 index 0000000000..1f44fc19e6 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/licenses/Popv1.txt @@ -0,0 +1,12 @@ + +

Proof of Provenance License (PoP License) v1.0

+
+

+ Subject to the terms and conditions of this license, the Copyright Holder grants a worldwide, non-exclusive, non-sublicensable, non-transferable (except by transfer of the Certificate or beneficial ownership thereof) license (i) to the Certificate Holder to display ONE COPY of the Item at a time across any and all virtual worlds WITHOUT MODIFICATION; (ii) to any party to view and interact with the Item as displayed by the Certificate Holder. Redistributions of source code must retain the all copyright notices. Notwithstanding the foregoing, modification of the Item may be permitted pursuant to terms provided in the Certificate. +

+

+ THE ITEM IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR A CONTRIBUTOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ITEM, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +

+

+ Reference to the “Certificate” means the Proof of Provenance Certificate containing a hash of the code used to generate the Item; ‘Item’ means the visual representation produced by the execution of the code hashed in the Certificate (which term includes the code itself); and “Certificate Holder” means a single holder of the private key for the Certificate. +

From 009f24dda1e63448f60e4e1b947303655e8525b2 Mon Sep 17 00:00:00 2001 From: Cristian Luis Duarte Date: Mon, 11 Feb 2019 21:28:31 -0300 Subject: [PATCH 08/43] Android - Make minWidth of message dialog wider so error messages like protocol version error fits in two lines --- interface/resources/qml/dialogs/MessageDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 6e576118d8..629027ab2a 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -75,7 +75,7 @@ ModalWindow { QtObject { id: d - readonly property int minWidth: 800 + readonly property int minWidth: 1100 readonly property int maxWidth: 1280 readonly property int minHeight: 120 readonly property int maxHeight: 720 From 61346437da1faf3e92707c16b4a56ff240c38d0b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Feb 2019 09:22:31 -0800 Subject: [PATCH 09/43] working on material mapping --- libraries/model-baker/CMakeLists.txt | 4 ++- libraries/model-networking/CMakeLists.txt | 3 ++ libraries/networking/src/ResourceCache.h | 8 ++--- .../render-utils/src/CauterizedModel.cpp | 1 + libraries/render-utils/src/Model.cpp | 31 ++++++++++++------- libraries/render-utils/src/Model.h | 1 + 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/libraries/model-baker/CMakeLists.txt b/libraries/model-baker/CMakeLists.txt index 1e67d04bc1..22c240b487 100644 --- a/libraries/model-baker/CMakeLists.txt +++ b/libraries/model-baker/CMakeLists.txt @@ -2,4 +2,6 @@ set(TARGET_NAME model-baker) setup_hifi_library() link_hifi_libraries(shared shaders task gpu graphics hfm material-networking) -include_hifi_library_headers(networking) \ No newline at end of file +include_hifi_library_headers(networking) +include_hifi_library_headers(image) +include_hifi_library_headers(ktx) \ No newline at end of file diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index 7701f54d43..e79d18f779 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -3,3 +3,6 @@ setup_hifi_library() link_hifi_libraries(shared shaders networking graphics fbx material-networking model-baker) include_hifi_library_headers(hfm) include_hifi_library_headers(task) +include_hifi_library_headers(gpu) +include_hifi_library_headers(image) +include_hifi_library_headers(ktx) \ No newline at end of file diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 4693bc0613..be514f48f0 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -365,7 +365,7 @@ public: virtual ~Resource(); virtual QString getType() const { return "Resource"; } - + /// Returns the key last used to identify this resource in the unused map. int getLRUKey() const { return _lruKey; } @@ -374,13 +374,13 @@ public: /// Sets the load priority for one owner. virtual void setLoadPriority(const QPointer& owner, float priority); - + /// Sets a set of priorities at once. virtual void setLoadPriorities(const QHash, float>& priorities); - + /// Clears the load priority for one owner. virtual void clearLoadPriority(const QPointer& owner); - + /// Returns the highest load priority across all owners. float getLoadPriority(); diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index d4baddadee..4df933c972 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -64,6 +64,7 @@ void CauterizedModel::createRenderItemSet() { _modelMeshRenderItems.clear(); _modelMeshMaterialNames.clear(); _modelMeshRenderItemShapes.clear(); + _priorityMap.clear(); Transform transform; transform.setTranslation(_translation); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index f278c5e2e9..7acdd10a8c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1434,6 +1434,7 @@ void Model::createRenderItemSet() { _modelMeshRenderItems.clear(); _modelMeshMaterialNames.clear(); _modelMeshRenderItemShapes.clear(); + _priorityMap.clear(); Transform transform; transform.setTranslation(_translation); @@ -1525,30 +1526,30 @@ void Model::applyMaterialMapping() { PrimitiveMode primitiveMode = getPrimitiveMode(); bool useDualQuaternionSkinning = _useDualQuaternionSkinning; - render::Transaction transaction; - std::unordered_map priorityMap; auto& materialMapping = getMaterialMapping(); - qDebug() << "boop" << materialMapping.size(); for (auto& mapping : materialMapping) { std::set shapeIDs = getMeshIDsFromMaterialID(QString(mapping.first.c_str())); - - qDebug() << "boop2" << mapping.first.c_str() << shapeIDs.size(); - if (shapeIDs.size() == 0) { + auto networkMaterialResource = mapping.second; + if (!networkMaterialResource || shapeIDs.size() == 0) { continue; } - auto networkMaterialResource = mapping.second; - qDebug() << (bool)networkMaterialResource; - if (networkMaterialResource && networkMaterialResource->isLoaded() && networkMaterialResource->parsedMaterials.names.size() > 0) { + auto materialLoaded = [this, networkMaterialResource, shapeIDs, renderItemsKey, primitiveMode, useDualQuaternionSkinning]() { + qDebug() << "boop2" << networkMaterialResource->isFailed() << networkMaterialResource->parsedMaterials.names.size(); + if (networkMaterialResource->isFailed() || networkMaterialResource->parsedMaterials.names.size() > 0) { + return; + } + render::Transaction transaction; auto networkMaterial = networkMaterialResource->parsedMaterials.networkMaterials[networkMaterialResource->parsedMaterials.names[0]]; for (auto shapeID : shapeIDs) { + qDebug() << "boop3" << shapeID << _modelMeshRenderItemIDs.size(); if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); - graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, ++priorityMap[shapeID]); + graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, ++_priorityMap[shapeID]); transaction.updateItem(itemID, [material, renderItemsKey, - invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { + invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.addMaterial(material); // if the material changed, we might need to update our item key or shape key data.updateKey(renderItemsKey); @@ -1556,9 +1557,15 @@ void Model::applyMaterialMapping() { }); } } + AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); + }; + qDebug() << "boop" << networkMaterialResource->isLoaded(); + if (networkMaterialResource->isLoaded()) { + materialLoaded(); + } else { + connect(networkMaterialResource.data(), &Resource::finished, materialLoaded); } } - AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); } void Model::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index c484a82705..c4d1feaa1b 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -374,6 +374,7 @@ signals: protected: + std::unordered_map _priorityMap; // only used for materialMapping void applyMaterialMapping(); void setBlendshapeCoefficients(const QVector& coefficients) { _blendshapeCoefficients = coefficients; } From 189ccfde4ae1cd7c27408cec46ff9f83c7d80a39 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Feb 2019 09:51:11 -0800 Subject: [PATCH 10/43] clement's comments from PR14858 --- libraries/animation/src/AnimationCache.cpp | 2 +- libraries/audio/src/SoundCache.cpp | 2 +- .../src/material-networking/MaterialCache.cpp | 2 +- .../src/material-networking/ShaderCache.cpp | 2 +- .../src/material-networking/TextureCache.cpp | 13 ++++++------- .../src/model-networking/ModelCache.cpp | 6 +++--- libraries/networking/src/ResourceCache.h | 10 +++++----- libraries/recording/src/recording/ClipCache.cpp | 2 +- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 4e988334f9..237fd3da02 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -41,7 +41,7 @@ QSharedPointer AnimationCache::createResource(const QUrl& url) { } QSharedPointer AnimationCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new Animation(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new Animation(*resource.staticCast()), &Resource::deleter); } AnimationReader::AnimationReader(const QUrl& url, const QByteArray& data) : diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp index 343de46e9a..c36897c766 100644 --- a/libraries/audio/src/SoundCache.cpp +++ b/libraries/audio/src/SoundCache.cpp @@ -40,5 +40,5 @@ QSharedPointer SoundCache::createResource(const QUrl& url) { } QSharedPointer SoundCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new Sound(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new Sound(*resource.staticCast()), &Resource::deleter); } \ No newline at end of file diff --git a/libraries/material-networking/src/material-networking/MaterialCache.cpp b/libraries/material-networking/src/material-networking/MaterialCache.cpp index bccf1ca0c4..1b5ba6f0f6 100644 --- a/libraries/material-networking/src/material-networking/MaterialCache.cpp +++ b/libraries/material-networking/src/material-networking/MaterialCache.cpp @@ -425,7 +425,7 @@ QSharedPointer MaterialCache::createResource(const QUrl& url) { } QSharedPointer MaterialCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkMaterialResource(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkMaterialResource(*resource.staticCast()), &Resource::deleter); } NetworkMaterial::NetworkMaterial(const NetworkMaterial& m) : diff --git a/libraries/material-networking/src/material-networking/ShaderCache.cpp b/libraries/material-networking/src/material-networking/ShaderCache.cpp index 8d060c42f2..4c8d659315 100644 --- a/libraries/material-networking/src/material-networking/ShaderCache.cpp +++ b/libraries/material-networking/src/material-networking/ShaderCache.cpp @@ -29,5 +29,5 @@ QSharedPointer ShaderCache::createResource(const QUrl& url) { } QSharedPointer ShaderCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkShader(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkShader(*resource.staticCast()), &Resource::deleter); } diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 2cc7a4b032..95cf7dfd90 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -197,16 +197,16 @@ public: namespace std { template <> struct hash { - size_t operator()(const QByteArray& a) const { - return qHash(a); + size_t operator()(const QByteArray& byteArray) const { + return qHash(byteArray); } }; template <> struct hash { - size_t operator()(const TextureExtra& a) const { + size_t operator()(const TextureExtra& textureExtra) const { size_t result = 0; - hash_combine(result, (int)a.type, a.content, a.maxNumPixels); + hash_combine(result, (int)textureExtra.type, textureExtra.content, textureExtra.maxNumPixels); return result; } }; @@ -328,14 +328,13 @@ QSharedPointer TextureCache::createResource(const QUrl& url) { } QSharedPointer TextureCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkTexture(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkTexture(*resource.staticCast()), &Resource::deleter); } int networkTexturePointerMetaTypeId = qRegisterMetaType>(); NetworkTexture::NetworkTexture(const QUrl& url, bool resourceTexture) : - Resource(url), - _maxNumPixels(100) + Resource(url) { if (resourceTexture) { _textureSource = std::make_shared(url); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 0ed518937d..1a4542d279 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -76,9 +76,9 @@ namespace std { template <> struct hash { - size_t operator()(const GeometryExtra& a) const { + size_t operator()(const GeometryExtra& geometryExtra) const { size_t result = 0; - hash_combine(result, a.mapping, a.textureBaseUrl, a.combineParts); + hash_combine(result, geometryExtra.mapping, geometryExtra.textureBaseUrl, geometryExtra.combineParts); return result; } }; @@ -394,7 +394,7 @@ QSharedPointer ModelCache::createResource(const QUrl& url) { } QSharedPointer ModelCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter); } GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index be514f48f0..9ff8070768 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -489,14 +489,14 @@ protected: QWeakPointer _self; QPointer _cache; - qint64 _bytesReceived{ 0 }; - qint64 _bytesTotal{ 0 }; - qint64 _bytes{ 0 }; + qint64 _bytesReceived { 0 }; + qint64 _bytesTotal { 0 }; + qint64 _bytes { 0 }; int _requestID; - ResourceRequest* _request{ nullptr }; + ResourceRequest* _request { nullptr }; - size_t _extraHash; + size_t _extraHash { std::numeric_limits::max() }; public slots: void handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTotal); diff --git a/libraries/recording/src/recording/ClipCache.cpp b/libraries/recording/src/recording/ClipCache.cpp index c08dd40ad8..bc20e4d8eb 100644 --- a/libraries/recording/src/recording/ClipCache.cpp +++ b/libraries/recording/src/recording/ClipCache.cpp @@ -54,5 +54,5 @@ QSharedPointer ClipCache::createResource(const QUrl& url) { } QSharedPointer ClipCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkClipLoader(*resource.staticCast().data()), &Resource::deleter); + return QSharedPointer(new NetworkClipLoader(*resource.staticCast()), &Resource::deleter); } \ No newline at end of file From 2728654d352ba22f7133e810afe16af96a97d916 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 12 Feb 2019 10:09:28 -0800 Subject: [PATCH 11/43] Fix duplicating non-dynamic grabbed entities in edit.js --- scripts/system/libraries/entitySelectionTool.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 01e5f6e22b..3e16315c6d 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -284,6 +284,8 @@ SelectionManager = (function() { properties.parentJointIndex = null; properties.localPosition = properties.position; properties.localRotation = properties.rotation; + properties.velocity = { x: 0, y: 0, z: 0 }; + properties.angularVelocity = { x: 0, y: 0, z: 0 }; } delete properties.actionData; var newEntityID = Entities.addEntity(properties); From f75a3e1a727bc8842701b9d33fd39f3574d91ac4 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Feb 2019 11:41:47 -0800 Subject: [PATCH 12/43] it's working! --- .../model-baker/ParseMaterialMappingTask.cpp | 60 ++++++++++++------- .../render-utils/src/CauterizedModel.cpp | 2 - libraries/render-utils/src/Model.cpp | 10 ++-- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index 7a4acb595a..b313593be2 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -25,35 +25,51 @@ void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, con } else { auto mappingKeys = materialMap.keys(); for (auto mapping : mappingKeys) { - auto mappingValue = materialMap[mapping].toObject(); + auto mappingJSON = materialMap[mapping]; + if (mappingJSON.isObject()) { + auto mappingValue = mappingJSON.toObject(); - // Old subsurface scattering mapping - { - auto scatteringIter = mappingValue.find("scattering"); - auto scatteringMapIter = mappingValue.find("scatteringMap"); - if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) { - std::shared_ptr material = std::make_shared(); + // Old subsurface scattering mapping + { + auto scatteringIter = mappingValue.find("scattering"); + auto scatteringMapIter = mappingValue.find("scatteringMap"); + if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) { + std::shared_ptr material = std::make_shared(); - if (scatteringIter != mappingValue.end()) { - float scattering = (float)scatteringIter.value().toDouble(); - material->setScattering(scattering); + if (scatteringIter != mappingValue.end()) { + float scattering = (float)scatteringIter.value().toDouble(); + material->setScattering(scattering); + } + + if (scatteringMapIter != mappingValue.end()) { + QString scatteringMap = scatteringMapIter.value().toString(); + material->setScatteringMap(scatteringMap); + } + + material->setDefaultFallthrough(true); + + NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); }); + materialResource->moveToThread(qApp->thread()); + materialResource->parsedMaterials.names.push_back("scattering"); + materialResource->parsedMaterials.networkMaterials["scattering"] = material; + + materialMapping.push_back(std::pair("mat::" + mapping.toStdString(), materialResource)); + continue; } + } - if (scatteringMapIter != mappingValue.end()) { - QString scatteringMap = scatteringMapIter.value().toString(); - material->setScatteringMap(scatteringMap); - } - - material->setDefaultFallthrough(true); - + // Material JSON description + { NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); }); materialResource->moveToThread(qApp->thread()); - materialResource->parsedMaterials.names.push_back("scattering"); - materialResource->parsedMaterials.networkMaterials["scattering"] = material; - - materialMapping.push_back(std::pair("mat::" + mapping.toStdString(), materialResource)); - continue; + // TODO: add baseURL to allow FSTs to reference relative files next to them + materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), QUrl()); + materialMapping.push_back(std::pair(mapping.toStdString(), materialResource)); } + + } else if (mappingJSON.isString()) { + auto mappingValue = mappingJSON.toString(); + materialMapping.push_back(std::pair(mapping.toStdString(), MaterialCache::instance().getMaterial(mappingValue))); } } } diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 4df933c972..b70925201a 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -64,7 +64,6 @@ void CauterizedModel::createRenderItemSet() { _modelMeshRenderItems.clear(); _modelMeshMaterialNames.clear(); _modelMeshRenderItemShapes.clear(); - _priorityMap.clear(); Transform transform; transform.setTranslation(_translation); @@ -98,7 +97,6 @@ void CauterizedModel::createRenderItemSet() { } } _blendshapeOffsetsInitialized = true; - applyMaterialMapping(); } else { Model::createRenderItemSet(); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 7acdd10a8c..c15941b465 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -956,6 +956,7 @@ bool Model::addToScene(const render::ScenePointer& scene, } if (somethingAdded) { + applyMaterialMapping(); _addedToScene = true; updateRenderItems(); _needsFixupInScene = false; @@ -973,6 +974,7 @@ void Model::removeFromScene(const render::ScenePointer& scene, render::Transacti _modelMeshRenderItems.clear(); _modelMeshMaterialNames.clear(); _modelMeshRenderItemShapes.clear(); + _priorityMap.clear(); _blendshapeOffsets.clear(); _blendshapeOffsetsInitialized = false; @@ -1434,7 +1436,6 @@ void Model::createRenderItemSet() { _modelMeshRenderItems.clear(); _modelMeshMaterialNames.clear(); _modelMeshRenderItemShapes.clear(); - _priorityMap.clear(); Transform transform; transform.setTranslation(_translation); @@ -1466,7 +1467,6 @@ void Model::createRenderItemSet() { } } _blendshapeOffsetsInitialized = true; - applyMaterialMapping(); } bool Model::isRenderable() const { @@ -1535,14 +1535,12 @@ void Model::applyMaterialMapping() { } auto materialLoaded = [this, networkMaterialResource, shapeIDs, renderItemsKey, primitiveMode, useDualQuaternionSkinning]() { - qDebug() << "boop2" << networkMaterialResource->isFailed() << networkMaterialResource->parsedMaterials.names.size(); - if (networkMaterialResource->isFailed() || networkMaterialResource->parsedMaterials.names.size() > 0) { + if (networkMaterialResource->isFailed() || networkMaterialResource->parsedMaterials.names.size() == 0) { return; } render::Transaction transaction; auto networkMaterial = networkMaterialResource->parsedMaterials.networkMaterials[networkMaterialResource->parsedMaterials.names[0]]; for (auto shapeID : shapeIDs) { - qDebug() << "boop3" << shapeID << _modelMeshRenderItemIDs.size(); if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; @@ -1559,7 +1557,7 @@ void Model::applyMaterialMapping() { } AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction); }; - qDebug() << "boop" << networkMaterialResource->isLoaded(); + if (networkMaterialResource->isLoaded()) { materialLoaded(); } else { From fdbcf4b2ea712d569716d6f587cbf3fa56f0965c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 12 Feb 2019 12:03:35 -0800 Subject: [PATCH 13/43] cleanup and expose mapped materials to getScriptableModel --- libraries/model-baker/src/model-baker/Baker.cpp | 3 +-- .../src/model-baker/ParseMaterialMappingTask.cpp | 5 +---- .../src/model-baker/ParseMaterialMappingTask.h | 2 +- libraries/render-utils/src/Model.cpp | 12 +++++++++++- libraries/render-utils/src/Model.h | 1 + 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 037cfb7912..dfb18eef86 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -155,8 +155,7 @@ namespace baker { const auto jointIndices = jointInfoOut.getN(2); // Parse material mapping - const auto materialMappingInputs = ParseMaterialMappingTask::Input(materials, mapping).asVarying(); - const auto materialMapping = model.addJob("ParseMaterialMapping", materialMappingInputs); + const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index b313593be2..7a923a3702 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -10,10 +10,7 @@ #include "ModelBakerLogging.h" -void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { - const auto& materialsIn = input.get0(); - const auto& mapping = input.get1(); - +void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& mapping, Output& output) { 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 3e48c00acf..69e00b0324 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -19,7 +19,7 @@ class ParseMaterialMappingTask { public: - using Input = baker::VaryingSet2, QVariantHash>; + using Input = QVariantHash; using Output = MaterialMapping; using JobModel = baker::Job::ModelIO; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index c15941b465..b4eee57d89 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -756,7 +756,16 @@ scriptable::ScriptableModelBase Model::getScriptableModel() { int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - result.appendMaterial(graphics::MaterialLayer(getGeometry()->getShapeMaterial(shapeID), 0), shapeID, _modelMeshMaterialNames[shapeID]); + auto& materialName = _modelMeshMaterialNames[shapeID]; + result.appendMaterial(graphics::MaterialLayer(getGeometry()->getShapeMaterial(shapeID), 0), shapeID, materialName); + + auto mappedMaterialIter = _materialMapping.find(shapeID); + if (mappedMaterialIter != _materialMapping.end()) { + auto mappedMaterials = mappedMaterialIter->second; + for (auto& mappedMaterial : mappedMaterials) { + result.appendMaterial(mappedMaterial, shapeID, materialName); + } + } shapeID++; } } @@ -1546,6 +1555,7 @@ void Model::applyMaterialMapping() { auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, ++_priorityMap[shapeID]); + _materialMapping[shapeID].push_back(material); transaction.updateItem(itemID, [material, renderItemsKey, invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.addMaterial(material); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index c4d1feaa1b..2b73ac0a28 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -375,6 +375,7 @@ signals: protected: std::unordered_map _priorityMap; // only used for materialMapping + std::unordered_map> _materialMapping; // generated during applyMaterialMapping void applyMaterialMapping(); void setBlendshapeCoefficients(const QVector& coefficients) { _blendshapeCoefficients = coefficients; } From a82221d2a5a086493ffe3e5948bbb502bdbc93a2 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Tue, 12 Feb 2019 13:20:47 -0800 Subject: [PATCH 14/43] Case 21118 - descending sorts don't work in new (qml) marketplace --- .../hifi/commerce/marketplace/Marketplace.qml | 40 ++++++---- .../hifi/commerce/marketplace/SortButton.qml | 76 ++++++++++--------- interface/src/commerce/QmlMarketplace.cpp | 8 +- interface/src/commerce/QmlMarketplace.h | 7 +- 4 files changed, 72 insertions(+), 59 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 65fe35d4ee..cdb8368296 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -31,8 +31,9 @@ Rectangle { id: root property string activeView: "initialize" - property int currentSortIndex: 0 + property int currentSortIndex: 1 property string sortString: "recent" + property bool isAscending: false property string categoryString: "" property string searchString: "" property bool keyboardEnabled: HMD.active @@ -503,6 +504,7 @@ Rectangle { "", "", root.sortString, + root.isAscending, WalletScriptingInterface.limitedCommerce, marketBrowseModel.currentPageToRetrieve, marketBrowseModel.itemsPerPage @@ -731,7 +733,7 @@ Rectangle { top: parent.top leftMargin: 20 } - width: root.isLoggedIn ? 322 : 242 + width: root.isLoggedIn ? 342 : 262 height: 36 radius: 4 @@ -742,27 +744,27 @@ Rectangle { id: sortModel ListElement { - name: "Name"; - glyph: ";" + name: "Name" sortString: "alpha" + ascending: true } ListElement { - name: "Date"; - glyph: ";"; - sortString: "recent"; + name: "Date" + sortString: "recent" + ascending: false } ListElement { - name: "Popular"; - glyph: ";"; - sortString: "likes"; + name: "Popular" + sortString: "likes" + ascending: false } ListElement { - name: "My Likes"; - glyph: ";"; - sortString: "my_likes"; + name: "My Likes" + sortString: "my_likes" + ascending: false } } @@ -788,10 +790,10 @@ Rectangle { currentIndex: 1; delegate: SortButton { - width: 80 + width: 85 height: parent.height - glyph: model.glyph + ascending: model.ascending text: model.name visible: root.isLoggedIn || model.sortString != "my_likes" @@ -799,6 +801,12 @@ Rectangle { checked: ListView.isCurrentItem onClicked: { + if(root.currentSortIndex == index) { + ascending = !ascending; + } else { + ascending = model.ascending; + } + root.isAscending = ascending; root.currentSortIndex = index; sortListView.positionViewAtIndex(index, ListView.Beginning); sortListView.currentIndex = index; @@ -807,7 +815,7 @@ Rectangle { } } highlight: Rectangle { - width: 80 + width: 85 height: parent.height color: hifi.colors.faintGray diff --git a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml index 37ad2735ce..e876842d89 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml @@ -28,58 +28,60 @@ Item { id: root; + property string ascGlyph: "\u2191" + property string descGlyph: "\u2193" + property string text: "" + property bool ascending: false + property bool checked: false + signal clicked() - property string glyph: ""; - property string text: ""; - property bool checked: false; - signal clicked(); - - width: childrenRect.width; - height: parent.height; + width: childrenRect.width + height: parent.height Rectangle { - anchors.top: parent.top; - anchors.left: parent.left; - height: parent.height; - width: 2; - color: hifi.colors.faintGray; - visible: index > 0; + anchors.top: parent.top + anchors.left: parent.left + height: parent.height + width: 2 + color: hifi.colors.faintGray + visible: index > 0 } - HiFiGlyphs { - id: buttonGlyph; - text: root.glyph; + RalewayRegular { + id: buttonGlyph + text: root.ascending ? root.ascGlyph : root.descGlyph // Size - size: 14; + size: 14 // Anchors - anchors.left: parent.left; - anchors.leftMargin: 0; - anchors.top: parent.top; - anchors.verticalCenter: parent.verticalCenter; - height: parent.height; - horizontalAlignment: Text.AlignHCenter; + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.top: parent.top + anchors.topMargin: 6 + anchors.bottom: parent.bottom + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop // Style - color: hifi.colors.lightGray; + color: hifi.colors.lightGray } RalewayRegular { - id: buttonText; - text: root.text; + id: buttonText + text: root.text // Text size - size: 14; + size: 14 // Style - color: hifi.colors.lightGray; - elide: Text.ElideRight; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + color: hifi.colors.lightGray + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter // Anchors - anchors.left: parent.left; - anchors.leftMargin: 20; - anchors.top: parent.top; - height: parent.height; + anchors.left: buttonGlyph.right + anchors.leftMargin: 5 + anchors.top: parent.top + height: parent.height } MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; + anchors.fill: parent + hoverEnabled: enabled onClicked: { root.clicked(); } diff --git a/interface/src/commerce/QmlMarketplace.cpp b/interface/src/commerce/QmlMarketplace.cpp index 23ba418a2d..8197b20275 100644 --- a/interface/src/commerce/QmlMarketplace.cpp +++ b/interface/src/commerce/QmlMarketplace.cpp @@ -50,9 +50,10 @@ void QmlMarketplace::getMarketplaceItems( const QString& adminFilter, const QString& adminFilterCost, const QString& sort, - const bool isFree, - const int& page, - const int& perPage) { + bool isAscending, + bool isFree, + int page, + int perPage) { QString endpoint = "items"; QUrlQuery request; @@ -62,6 +63,7 @@ void QmlMarketplace::getMarketplaceItems( request.addQueryItem("adminFilter", adminFilter); request.addQueryItem("adminFilterCost", adminFilterCost); request.addQueryItem("sort", sort); + request.addQueryItem("sort_dir", isAscending ? "asc" : "desc"); if (isFree) { request.addQueryItem("isFree", "true"); } diff --git a/interface/src/commerce/QmlMarketplace.h b/interface/src/commerce/QmlMarketplace.h index 5794d4f53c..76b3d41449 100644 --- a/interface/src/commerce/QmlMarketplace.h +++ b/interface/src/commerce/QmlMarketplace.h @@ -46,9 +46,10 @@ protected: const QString& adminFilter = QString("published"), const QString& adminFilterCost = QString(), const QString& sort = QString(), - const bool isFree = false, - const int& page = 1, - const int& perPage = 20); + bool isAscending = false, + bool isFree = false, + int page = 1, + int perPage = 20); Q_INVOKABLE void getMarketplaceItem(const QString& marketplaceItemId); Q_INVOKABLE void marketplaceItemLike(const QString& marketplaceItemId, const bool like = true); Q_INVOKABLE void getMarketplaceCategories(); From a967d7a1bc8760e647be2db86aa0e3a9d2c14189 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Wed, 13 Feb 2019 15:59:55 -0800 Subject: [PATCH 15/43] tangent attribute --- libraries/fbx/src/GLTFSerializer.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 82a4361723..da77ecd77b 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -892,7 +892,24 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { for (int n = 0; n < colors.size() - 3; n += stride) { mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); } - } else if (key == "TEXCOORD_0") { + } else if (key == "TANGENT") { + QVector tangents; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + tangents, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF TANGENT data for model " << _url; + continue; + } + int stride = (accessor.type == GLTFAccessorType::VEC4) ? 4 : 3; + for (int n = 0; n < tangents.size() - 3; n += stride) { + float tanW = stride == 4 ? tangents[n + 3] : 1; + mesh.tangents.push_back(glm::vec3(tanW * tangents[n], tangents[n + 1], tangents[n + 2])); + } + } else if (key == "TEXCOORD_0") { QVector texcoords; success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, @@ -931,7 +948,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { } mesh.parts.push_back(part); - // populate the texture coordenates if they don't exist + // populate the texture coordinates if they don't exist if (mesh.texCoords.size() == 0) { for (int i = 0; i < part.triangleIndices.size(); i++) mesh.texCoords.push_back(glm::vec2(0.0, 1.0)); } From bd3a732cdcd76e750127b2f49aa29a9e72f3520e Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Wed, 13 Feb 2019 17:38:59 -0800 Subject: [PATCH 16/43] Introducing msaa to forward renderer --- .../render-utils/src/RenderCommonTask.cpp | 68 ++++++++++++++++++- libraries/render-utils/src/RenderCommonTask.h | 22 ++++++ .../render-utils/src/RenderForwardTask.cpp | 22 ++++-- .../render-utils/src/RenderForwardTask.h | 23 ++++++- 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 40724cbf5a..b4a77479db 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -197,7 +197,73 @@ void Blit::run(const RenderContextPointer& renderContext, const gpu::Framebuffer }); } -void ExtractFrustums::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& output) { + +void ResolveFramebuffer::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { + RenderArgs* args = renderContext->args; + auto srcFbo = inputs.get0(); + auto destFbo = inputs.get1(); + + if (!destFbo) { + destFbo = args->_blitFramebuffer; + } + outputs = destFbo; + + // Check valid src and dest + if (!srcFbo || !destFbo) { + return; + } + + // Check valid size for sr and dest + auto frameSize(srcFbo->getSize()); + if (destFbo->getSize() != frameSize) { + return; + } + + gpu::Vec4i rectSrc; + rectSrc.z = frameSize.x; + rectSrc.w = frameSize.y; + gpu::doInBatch("Resolve", args->_context, [&](gpu::Batch& batch) { + batch.blit(srcFbo, rectSrc, destFbo, rectSrc); + }); +} + +void ResolveNewFramebuffer::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { + RenderArgs* args = renderContext->args; + auto srcFbo = inputs; + outputs.reset(); + + // Check valid src + if (!srcFbo) { + return; + } + + // Check valid size for sr and dest + auto frameSize(srcFbo->getSize()); + + // Resizing framebuffers instead of re-building them seems to cause issues with threaded rendering + if (_outputFramebuffer && _outputFramebuffer->getSize() != frameSize) { + _outputFramebuffer.reset(); + } + + if (!_outputFramebuffer) { + _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("resolvedNew.out")); + auto colorFormat = gpu::Element::COLOR_SRGBA_32; + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); + auto colorTexture = gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + _outputFramebuffer->setRenderBuffer(0, colorTexture); + } + + gpu::Vec4i rectSrc; + rectSrc.z = frameSize.x; + rectSrc.w = frameSize.y; + gpu::doInBatch("ResolveNew", args->_context, [&](gpu::Batch& batch) { batch.blit(srcFbo, rectSrc, _outputFramebuffer, rectSrc); }); + + outputs = _outputFramebuffer; +} + + + + void ExtractFrustums::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& output) { assert(renderContext->args); assert(renderContext->args->_context); diff --git a/libraries/render-utils/src/RenderCommonTask.h b/libraries/render-utils/src/RenderCommonTask.h index 29f195ffff..a1de50abba 100644 --- a/libraries/render-utils/src/RenderCommonTask.h +++ b/libraries/render-utils/src/RenderCommonTask.h @@ -90,6 +90,28 @@ public: void run(const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer); }; + +class ResolveFramebuffer { +public: + using Inputs = render::VaryingSet2; + using Outputs = gpu::FramebufferPointer; + using JobModel = render::Job::ModelIO; + + void run(const render::RenderContextPointer& renderContext, const Inputs& source, Outputs& dest); +}; + +class ResolveNewFramebuffer { +public: + using Inputs = gpu::FramebufferPointer; + using Outputs = gpu::FramebufferPointer; + using JobModel = render::Job::ModelIO; + + void run(const render::RenderContextPointer& renderContext, const Inputs& source, Outputs& dest); +private: + gpu::FramebufferPointer _outputFramebuffer; +}; + + class ExtractFrustums { public: diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index ffdbc1c4b1..c47935c6fc 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -129,10 +129,16 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend task.addJob("DrawZoneStack", debugZoneInputs); } + // Just resolve the msaa +/* const auto resolveInputs = + ResolveFramebuffer::Inputs(framebuffer, static_cast(nullptr)).asVarying(); + auto resolvedFramebuffer = task.addJob("Resolve", resolveInputs); */ + auto resolvedFramebuffer = task.addJob("Resolve", framebuffer); + // Lighting Buffer ready for tone mapping // Forward rendering on GLES doesn't support tonemapping to and from the same FBO, so we specify // the output FBO as null, which causes the tonemapping to target the blit framebuffer - const auto toneMappingInputs = ToneMappingDeferred::Inputs(framebuffer, static_cast(nullptr) ).asVarying(); + const auto toneMappingInputs = ToneMappingDeferred::Inputs(resolvedFramebuffer, static_cast(nullptr)).asVarying(); task.addJob("ToneMapping", toneMappingInputs); // Layered Overlays @@ -144,26 +150,32 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // task.addJob("Blit", framebuffer); } +void PrepareFramebuffer::configure(const Config& config) { + _numSamples = config.getNumSamples(); +} + void PrepareFramebuffer::run(const RenderContextPointer& renderContext, gpu::FramebufferPointer& framebuffer) { glm::uvec2 frameSize(renderContext->args->_viewport.z, renderContext->args->_viewport.w); // Resizing framebuffers instead of re-building them seems to cause issues with threaded rendering - if (_framebuffer && _framebuffer->getSize() != frameSize) { + if (_framebuffer && (_framebuffer->getSize() != frameSize || _framebuffer->getNumSamples() != _numSamples)) { _framebuffer.reset(); } if (!_framebuffer) { _framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("forward")); + int numSamples = _numSamples; + auto colorFormat = gpu::Element::COLOR_SRGBA_32; - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); auto colorTexture = - gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + gpu::Texture::createRenderBufferMultisample(colorFormat, frameSize.x, frameSize.y, numSamples, defaultSampler); _framebuffer->setRenderBuffer(0, colorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format auto depthTexture = - gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + gpu::Texture::createRenderBufferMultisample(depthFormat, frameSize.x, frameSize.y, numSamples, defaultSampler); _framebuffer->setDepthStencilBuffer(depthTexture, depthFormat); } diff --git a/libraries/render-utils/src/RenderForwardTask.h b/libraries/render-utils/src/RenderForwardTask.h index e6a6008319..85b51ad5fa 100755 --- a/libraries/render-utils/src/RenderForwardTask.h +++ b/libraries/render-utils/src/RenderForwardTask.h @@ -27,16 +27,37 @@ public: void build(JobModel& task, const render::Varying& input, render::Varying& output); }; + +class PrepareFramebufferConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(int numSamples WRITE setNumSamples READ getNumSamples NOTIFY dirty) +public: + int getNumSamples() const { return numSamples; } + void setNumSamples(int num) { + numSamples = std::max(1, std::min(32, num)); + emit dirty(); + } + +signals: + void dirty(); + +protected: + int numSamples{ 4 }; +}; + class PrepareFramebuffer { public: using Inputs = gpu::FramebufferPointer; - using JobModel = render::Job::ModelO; + using Config = PrepareFramebufferConfig; + using JobModel = render::Job::ModelO; + void configure(const Config& config); void run(const render::RenderContextPointer& renderContext, gpu::FramebufferPointer& framebuffer); private: gpu::FramebufferPointer _framebuffer; + int _numSamples; }; class PrepareForward { From 3026fd625a5f0242fc8b129071e4374f59e289a8 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 14 Feb 2019 18:52:50 +0100 Subject: [PATCH 17/43] Avatar Doctor --- .../avatarPackager/AvatarDoctorDiagnose.qml | 125 ++++++++++++++++++ .../AvatarDoctorErrorReport.qml | 112 ++++++++++++++++ .../hifi/avatarPackager/AvatarPackagerApp.qml | 32 ++++- .../avatarPackager/AvatarPackagerState.qml | 2 + .../hifi/avatarPackager/AvatarProjectCard.qml | 17 ++- interface/src/avatar/AvatarDoctor.cpp | 101 ++++++++++++++ interface/src/avatar/AvatarDoctor.h | 50 +++++++ interface/src/avatar/AvatarPackager.cpp | 11 +- interface/src/avatar/AvatarPackager.h | 11 +- interface/src/avatar/AvatarProject.cpp | 6 + interface/src/avatar/AvatarProject.h | 9 ++ 11 files changed, 468 insertions(+), 8 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml create mode 100644 interface/src/avatar/AvatarDoctor.cpp create mode 100644 interface/src/avatar/AvatarDoctor.h diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml new file mode 100644 index 0000000000..d329b903bd --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml @@ -0,0 +1,125 @@ +import QtQuick 2.0 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Item { + id: diagnosingScreen + + visible: false + + property var avatarDoctor: null + property var errors: [] + + signal doneDiagnosing + + onVisibleChanged: { + if (!diagnosingScreen.visible) { + //if (debugDelay.running) { + // debugDelay.stop(); + //} + return; + } + //debugDelay.start(); + avatarDoctor = AvatarPackagerCore.currentAvatarProject.diagnose(); + avatarDoctor.complete.connect(function(errors) { + console.warn("avatarDoctor.complete " + JSON.stringify(errors)); + diagnosingScreen.errors = errors; + AvatarPackagerCore.currentAvatarProject.hasErrors = errors.length > 0; + AvatarPackagerCore.addCurrentProjectToRecentProjects(); + + // FIXME: can't seem to change state here so do it with a timer instead + doneTimer.start(); + }); + avatarDoctor.startDiagnosing(); + } + + Timer { + id: doneTimer + interval: 1 + repeat: false + running: false + onTriggered: { + doneDiagnosing(); + } + } + +/* + Timer { + id: debugDelay + interval: 5000 + repeat: false + running: false + onTriggered: { + if (Math.random() > 0.5) { + // ERROR + avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport; + } else { + // SUCCESS + avatarPackager.state = AvatarPackagerState.project; + } + } + } +*/ + + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: cancelButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + height: 30 + width: 133 + text: qsTr("Cancel") + onClicked: { + avatarPackager.state = AvatarPackagerState.main; + } + } + } + + LoadingCircle { + id: loadingCircle + anchors { + top: parent.top + topMargin: 46 + horizontalCenter: parent.horizontalCenter + } + width: 163 + height: 163 + } + + RalewayRegular { + id: testingPackageTitle + + anchors { + horizontalCenter: parent.horizontalCenter + top: loadingCircle.bottom + topMargin: 5 + } + + text: "Testing package for errors" + size: 28 + color: "white" + } + + RalewayRegular { + id: testingPackageText + + anchors { + top: testingPackageTitle.bottom + topMargin: 26 + left: parent.left + leftMargin: 21 + right: parent.right + rightMargin: 16 + } + + text: "We are trying to find errors in your project so you can quickly understand and resolve them." + size: 21 + color: "white" + lineHeight: 33 + lineHeightMode: Text.FixedHeight + wrapMode: Text.Wrap + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml new file mode 100644 index 0000000000..8811ba48a3 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml @@ -0,0 +1,112 @@ +import QtQuick 2.0 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Item { + id: errorReport + + visible: false + + property alias errors: errorRepeater.model + + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: tryAgainButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: continueButton.left + anchors.rightMargin: 22 + height: 40 + width: 134 + text: qsTr("Try Again") + // colorScheme: root.colorScheme + onClicked: { + avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; + } + } + + HifiControls.Button { + id: continueButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + height: 40 + width: 133 + text: qsTr("Continue") + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: { + avatarPackager.state = AvatarPackagerState.project; + } + } + } + + HiFiGlyphs { + id: errorReportIcon + text: hifi.glyphs.alert + size: 315 + color: "#EA4C5F" + anchors { + top: parent.top + //topMargin: 73 + horizontalCenter: parent.horizontalCenter + } + } + + Column { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + top: errorReportIcon.bottom + topMargin: 27 + leftMargin: 13 + rightMargin: 13 + } + spacing: 7 + + Repeater { + id: errorRepeater + /*model: [ + {message: "Rig is not Hifi compatible", url: "http://www.highfidelity.com/"}, + {message: "Bone limit exceeds 256", url: "http://www.highfidelity.com/2"}, + {message: "Unsupported Texture", url: "http://www.highfidelity.com/texture"}, + {message: "Rig is not Hifi compatible", url: "http://www.highfidelity.com/"}, + {message: "Bone limit exceeds 256", url: "http://www.highfidelity.com/2"}, + {message: "Unsupported Texture", url: "http://www.highfidelity.com/texture"} + ]*/ + + Item { + height: 37 + width: parent.width + + HiFiGlyphs { + id: errorIcon + text: hifi.glyphs.alert + size: 56 + color: "#EA4C5F" + anchors { + top: parent.top + left: parent.left + } + } + + RalewayRegular { + id: errorLink + anchors { + top: parent.top + left: errorIcon.right + right: parent.right + } + linkColor: "#00B4EF"// style.colors.blueHighlight + size: 28 + text: "" + modelData.message + "" + onLinkActivated: Qt.openUrlExternally(modelData.url) + } + } + } + } + + +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index b4293d5eee..8afc60fd90 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -143,6 +143,18 @@ Item { PropertyChanges { target: createAvatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } }, + State { + name: AvatarPackagerState.avatarDoctorDiagnose + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarDoctorDiagnose; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarDoctorDiagnose.footer } + }, + State { + name: AvatarPackagerState.avatarDoctorErrorReport + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarDoctorErrorReport; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarDoctorErrorReport.footer } + }, State { name: AvatarPackagerState.project PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true } @@ -168,7 +180,7 @@ Item { return status; } avatarProject.reset(); - avatarPackager.state = AvatarPackagerState.project; + avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; return status; } @@ -242,6 +254,23 @@ Item { color: "#404040" } + AvatarDoctorDiagnose { + id: avatarDoctorDiagnose + anchors.fill: parent + onErrorsChanged: { + avatarDoctorErrorReport.errors = avatarDoctorDiagnose.errors; + } + onDoneDiagnosing: { + avatarPackager.state = avatarDoctorDiagnose.errors.length > 0 ? AvatarPackagerState.avatarDoctorErrorReport + : AvatarPackagerState.project; + } + } + + AvatarDoctorErrorReport { + id: avatarDoctorErrorReport + anchors.fill: parent + } + AvatarProject { id: avatarProject colorScheme: root.colorScheme @@ -383,6 +412,7 @@ Item { title: modelData.name path: modelData.projectPath onOpen: avatarPackager.openProject(modelData.path) + hasError: modelData.hadErrors } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml index c81173a080..4a5abbb04b 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml @@ -7,4 +7,6 @@ Item { readonly property string project: "project" readonly property string createProject: "createProject" readonly property string projectUpload: "projectUpload" + readonly property string avatarDoctorDiagnose: "avatarDoctorDiagnose" + readonly property string avatarDoctorErrorReport: "avatarDoctorErrorReport" } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index 25222c814c..21d0683fb1 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -12,6 +12,7 @@ Item { property alias title: title.text property alias path: path.text + property alias hasError: errorIcon.visible property color textColor: "#E3E3E3" property color hoverTextColor: "#121212" @@ -54,7 +55,7 @@ Item { RalewayBold { id: title - elide: "ElideRight" + elide: Text.ElideRight anchors { top: parent.top topMargin: 13 @@ -76,12 +77,24 @@ Item { right: background.right rightMargin: 16 } - elide: "ElideLeft" + elide: Text.ElideLeft horizontalAlignment: Text.AlignRight text: "" size: 20 } + HiFiGlyphs { + id: errorIcon + visible: false + text: hifi.glyphs.alert + size: 56 + color: "#EA4C5F" + anchors { + top: parent.top + right: parent.right + } + } + MouseArea { id: mouseArea anchors.fill: parent diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp new file mode 100644 index 0000000000..d2397ed21f --- /dev/null +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -0,0 +1,101 @@ +// +// AvatarDoctor.cpp +// +// +// Created by Thijs Wenker on 2/12/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 "AvatarDoctor.h" +#include + +AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : + _avatarFSTFileUrl(std::move(avatarFSTFileUrl)) { +} + +void AvatarDoctor::startDiagnosing() { + _errors.clear(); + const auto resource = DependencyManager::get()->getGeometryResource(_avatarFSTFileUrl); + const auto resourceLoaded = [this, resource](bool success) { + // MODEL + if (!success) { + _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + emit complete(getErrors()); + return; + } + const auto avatarModel = resource.data()->getHFMModel(); + if (!avatarModel.originalURL.endsWith(".fbx")) { + _errors.push_back({ "Unsupported avatar model format", QUrl("http://www.highfidelity.com/docs") }); + emit complete(getErrors()); + return; + } + + // RIG + if (avatarModel.joints.isEmpty()) { + _errors.push_back({ "Avatar has no rig", QUrl("http://www.highfidelity.com/docs") }); + } + else { + if (avatarModel.joints.length() > 256) { + _errors.push_back({ "Avatar has over 256 bones", QUrl("http://www.highfidelity.com/docs") }); + } + // Avatar does not have Hips bone mapped + if (!avatarModel.getJointNames().contains("Hips")) { + _errors.push_back({ "Hips are not mapped", QUrl("http://www.highfidelity.com/docs") }); + } + if (!avatarModel.getJointNames().contains("Spine")) { + _errors.push_back({ "Spine is not mapped", QUrl("http://www.highfidelity.com/docs") }); + } + if (!avatarModel.getJointNames().contains("Head")) { + _errors.push_back({ "Head is not mapped", QUrl("http://www.highfidelity.com/docs") }); + } + } + + // SCALE + const float DEFAULT_HEIGHT = 1.75f; + const float RECOMMENDED_MIN_HEIGHT = DEFAULT_HEIGHT * 0.25; + const float RECOMMENDED_MAX_HEIGHT = DEFAULT_HEIGHT * 1.5; + + float avatarHeight = avatarModel.getMeshExtents().largestDimension(); + + qWarning() << "avatarHeight" << avatarHeight; + if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { + _errors.push_back({ "Avatar is possibly smaller then expected.", QUrl("http://www.highfidelity.com/docs") }); + } + else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { + _errors.push_back({ "Avatar is possibly larger then expected.", QUrl("http://www.highfidelity.com/docs") }); + } + + // BLENDSHAPES + + // TEXTURES + //avatarModel.materials. + + + emit complete(getErrors()); + }; + + if (resource) { + if (resource->isLoaded()) { + resourceLoaded(!resource->isFailed()); + } else { + connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); + } + } else { + _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + emit complete(getErrors()); + } +} + +QVariantList AvatarDoctor::getErrors() const { + QVariantList result; + for (const auto& error : _errors) { + QVariantMap errorVariant; + errorVariant.insert("message", error.message); + errorVariant.insert("url", error.url); + result.append(errorVariant); + } + return result; +} diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h new file mode 100644 index 0000000000..65a184af71 --- /dev/null +++ b/interface/src/avatar/AvatarDoctor.h @@ -0,0 +1,50 @@ +// +// AvatarDoctor.h +// +// +// Created by Thijs Wenker on 02/12/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 +// + +#pragma once +#ifndef hifi_AvatarDoctor_h +#define hifi_AvatarDoctor_h + +#include +#include +#include +#include + +struct AvatarDiagnosticResult { + +//public: + // AvatarDiagnosticResult() {} + // AvatarDiagnosticResult(QString message, QUrl url) : _message(std::move(message)), _url(std::move(url)) { } +//private: + QString message; + QUrl url; +}; +Q_DECLARE_METATYPE(AvatarDiagnosticResult) +Q_DECLARE_METATYPE(QVector) + +class AvatarDoctor : public QObject { + Q_OBJECT +public: + AvatarDoctor(QUrl avatarFSTFileUrl); + + Q_INVOKABLE void startDiagnosing(); + + Q_INVOKABLE QVariantList getErrors() const; + +signals: + void complete(QVariantList errors); + +private: + QUrl _avatarFSTFileUrl; + QVector _errors; +}; + +#endif // hifi_AvatarDoctor_h diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index fa70eee374..24f31cac9c 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -31,6 +31,9 @@ AvatarPackager::AvatarPackager() { qmlRegisterType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); qRegisterMetaType(); qmlRegisterUncreatableMetaObject( AvatarProjectStatus::staticMetaObject, @@ -84,7 +87,7 @@ void AvatarPackager::addCurrentProjectToRecentProjects() { _recentProjects.removeOne(removeProject); } - const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath); + const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath, _currentAvatarProject->getHasErrors()); _recentProjects.prepend(newRecentProject); while (_recentProjects.size() > MAX_RECENT_PROJECTS) { @@ -101,6 +104,7 @@ QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPath QVariantMap projectVariant; projectVariant.insert("name", project.getProjectName()); projectVariant.insert("path", project.getProjectFSTPath()); + projectVariant.insert("hadErrors", project.getHadErrors()); if (includeProjectPaths) { projectVariant.insert("projectPath", project.getProjectPath()); } @@ -113,7 +117,10 @@ void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) _recentProjects.clear(); for (const auto& projectVariant : projectsVariant) { auto map = projectVariant.toMap(); - _recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString())); + _recentProjects.append(RecentAvatarProject( + map.value("name").toString(), + map.value("path").toString(), + map.value("hadErrors", false).toBool())); } } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index ec954a60d7..13f62cb471 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -26,19 +26,23 @@ public: RecentAvatarProject() = default; - RecentAvatarProject(QString projectName, QString projectFSTPath) { + RecentAvatarProject(QString projectName, QString projectFSTPath, bool hadErrors) { _projectName = projectName; _projectFSTPath = projectFSTPath; + _hadErrors = hadErrors; } RecentAvatarProject(const RecentAvatarProject& other) { _projectName = other._projectName; _projectFSTPath = other._projectFSTPath; + _hadErrors = other._hadErrors; } QString getProjectName() const { return _projectName; } QString getProjectFSTPath() const { return _projectFSTPath; } + bool getHadErrors() const { return _hadErrors; } + QString getProjectPath() const { return QFileInfo(_projectFSTPath).absoluteDir().absolutePath(); } @@ -50,6 +54,7 @@ public: private: QString _projectName; QString _projectFSTPath; + bool _hadErrors; }; @@ -73,6 +78,8 @@ public: return AvatarProject::isValidNewProjectName(projectPath, projectName); } + Q_INVOKABLE void addCurrentProjectToRecentProjects(); + signals: void avatarProjectChanged(); void recentProjectsChanged(); @@ -84,8 +91,6 @@ private: void setAvatarProject(AvatarProject* avatarProject); - void addCurrentProjectToRecentProjects(); - AvatarProject* _currentAvatarProject { nullptr }; QVector _recentProjects; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 728917e673..74edabd1f5 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -243,6 +243,12 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { return uploader; } +AvatarDoctor* AvatarProject::diagnose() { + auto avatarDoctor = new AvatarDoctor(QUrl(getFSTPath())); + + return avatarDoctor; +} + void AvatarProject::openInInventory() const { constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 }; diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 1710282a3e..f11547bdca 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -14,6 +14,7 @@ #define hifi_AvatarProject_h #include "MarketplaceItemUploader.h" +#include "AvatarDoctor.h" #include "ProjectFile.h" #include "FST.h" @@ -53,11 +54,14 @@ class AvatarProject : public QObject { Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT) Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT) Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged) + Q_PROPERTY(bool hasErrors READ getHasErrors WRITE setHasErrors NOTIFY hasErrorsChanged) public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory() const; Q_INVOKABLE QStringList getProjectFiles() const; + Q_INVOKABLE AvatarDoctor* diagnose(); + Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } Q_INVOKABLE void setProjectName(const QString& newProjectName) { @@ -72,6 +76,8 @@ public: Q_INVOKABLE QString getFBXPath() const { return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath())); } + Q_INVOKABLE bool getHasErrors() const { return _hasErrors; } + Q_INVOKABLE void setHasErrors(bool hasErrors) { _hasErrors = hasErrors; } /** * returns the AvatarProject or a nullptr on failure. @@ -92,6 +98,7 @@ public: signals: void nameChanged(); void projectFilesChanged(); + void hasErrorsChanged(); private: AvatarProject(const QString& fstPath, const QByteArray& data); @@ -110,6 +117,8 @@ private: QDir _directory; QList _projectFiles{}; QString _projectPath; + + bool _hasErrors { false }; }; #endif // hifi_AvatarProject_h From 2a338124aec750340617bbbff8766a224d2d61c1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Feb 2019 08:00:19 +1300 Subject: [PATCH 18/43] AccountServices API JSDoc --- .../AccountServicesScriptingInterface.cpp | 6 ++ .../AccountServicesScriptingInterface.h | 75 +++++++++++++++---- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/interface/src/scripting/AccountServicesScriptingInterface.cpp b/interface/src/scripting/AccountServicesScriptingInterface.cpp index 3939fce91d..a3597886e9 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.cpp +++ b/interface/src/scripting/AccountServicesScriptingInterface.cpp @@ -112,6 +112,12 @@ DownloadInfoResult::DownloadInfoResult() : { } +/**jsdoc + * Information on the assets currently being downloaded and pending download. + * @typedef {object} AccountServices.DownloadInfoResult + * @property {number[]} downloading - The percentage complete for each asset currently being downloaded. + * @property {number} pending - The number of assets waiting to be download. + */ QScriptValue DownloadInfoResultToScriptValue(QScriptEngine* engine, const DownloadInfoResult& result) { QScriptValue object = engine->newObject(); diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index fb3c329def..2234583641 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -38,16 +38,24 @@ class AccountServicesScriptingInterface : public QObject { Q_OBJECT /**jsdoc - * The AccountServices API contains helper functions related to user connectivity + * The AccountServices API provides functions related to user connectivity, visibility, and asset download + * progress. * * @hifi-interface * @hifi-client-entity * * @namespace AccountServices - * @property {string} username Read-only. - * @property {boolean} loggedIn Read-only. - * @property {string} findableBy - * @property {string} metaverseServerURL Read-only. + * @property {string} username - The user name if the user is logged in, otherwise "Unknown user". + * Read-only. + * @property {boolean} loggedIn - true if the user is logged in, otherwise false. + * Read-only. + * @property {string} findableBy - The user's visibility to other people:
+ * "none" - user appears offline.
+ * "friends" - user is visible only to friends.
+ * "connections" - user is visible to friends and connections.
+ * "all" - user is visible to everyone. + * @property {string} metaverseServerURL - The metaverse server that the user is authenticated against when logged in + * — typically "https://metaverse.highfidelity.com". Read-only. */ Q_PROPERTY(QString username READ getUsername NOTIFY myUsernameChanged) @@ -65,29 +73,38 @@ public: public slots: /**jsdoc + * Get information on the progress of downloading assets in the domain. * @function AccountServices.getDownloadInfo - * @returns {DownloadInfoResult} + * @returns {AccountServices.DownloadInfoResult} Information on the progress of assets download. */ DownloadInfoResult getDownloadInfo(); /**jsdoc + * Cause a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal to be triggered with information on the + * current progress of the download of assets in the domain. * @function AccountServices.updateDownloadInfo */ void updateDownloadInfo(); /**jsdoc + * Check whether the user is logged in. * @function AccountServices.isLoggedIn - * @returns {boolean} + * @returns {boolean} true if the user is logged in, false otherwise. + * @example Report whether you are logged in. + * var isLoggedIn = AccountServices.isLoggedIn(); + * print("You are logged in: " + isLoggedIn); // true or false */ bool isLoggedIn(); /**jsdoc + * Prompts the user to log in (the login dialog is displayed) if they're not already logged in. * @function AccountServices.checkAndSignalForAccessToken - * @returns {boolean} + * @returns {boolean} true if the user is already logged in, false otherwise. */ bool checkAndSignalForAccessToken(); /**jsdoc + * Logs the user out. * @function AccountServices.logOut */ void logOut(); @@ -105,43 +122,75 @@ private slots: signals: /**jsdoc + * Not currently used. * @function AccountServices.connected * @returns {Signal} */ void connected(); /**jsdoc + * Triggered when the user logs out. * @function AccountServices.disconnected - * @param {string} reason + * @param {string} reason - Has the value, "logout". * @returns {Signal} */ void disconnected(const QString& reason); /**jsdoc + * Triggered when the username logged in with changes, i.e., when the user logs in or out. * @function AccountServices.myUsernameChanged - * @param {string} username + * @param {string} username - The username logged in with if the user is logged in, otherwise "". * @returns {Signal} + * @example Report when your username changes. + * AccountServices.myUsernameChanged.connect(function (username) { + * print("Username changed: " + username); + * }); */ void myUsernameChanged(const QString& username); /**jsdoc + * Triggered when the progress of the download of assets for the domain changes. * @function AccountServices.downloadInfoChanged - * @param {} info + * @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the progress of assets download. * @returns {Signal} */ void downloadInfoChanged(DownloadInfoResult info); /**jsdoc + * Triggered when the user's visibility to others changes. * @function AccountServices.findableByChanged - * @param {string} discoverabilityMode + * @param {string} findableBy - The user's visibility to other people:
+ * "none" - user appears offline.
+ * "friends" - user is visible only to friends.
+ * "connections" - user is visible to friends and connections.
+ * "all" - user is visible to everyone. * @returns {Signal} + * @example Report when your visiblity changes. + * AccountServices.findableByChanged.connect(function (findableBy) { + * print("Findable by changed: " + findableBy); + * }); + * + * var originalFindableBy = AccountServices.findableBy; + * Script.setTimeout(function () { + * // Change visiblity. + * AccountServices.findableBy = originalFindableBy === "none" ? "all" : "none"; + * }, 2000); + * Script.setTimeout(function () { + * // Restore original visibility. + * AccountServices.findableBy = originalFindableBy; + * }, 4000); */ void findableByChanged(const QString& discoverabilityMode); /**jsdoc + * Triggered when the login status of the user changes. * @function AccountServices.loggedInChanged - * @param {boolean} loggedIn + * @param {boolean} loggedIn - true if the user is logged in, otherwise false. * @returns {Signal} + * @example Report when your login status changes. + * AccountServices.loggedInChanged.connect(function(loggedIn) { + * print("Logged in: " + loggedIn); + * }); */ void loggedInChanged(bool loggedIn); From cc083afec60c1f95b799fdfc90844894c6e6fec1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Feb 2019 08:00:36 +1300 Subject: [PATCH 19/43] HifiAbout API JSDoc --- interface/src/AboutUtil.h | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/interface/src/AboutUtil.h b/interface/src/AboutUtil.h index 767e69842d..0d56f914b5 100644 --- a/interface/src/AboutUtil.h +++ b/interface/src/AboutUtil.h @@ -16,14 +16,22 @@ #include /**jsdoc + * The HifiAbout API provides information about the version of Interface that is currently running. It also + * provides the ability to open a Web page in an Interface browser window. + * * @namespace HifiAbout * * @hifi-interface * @hifi-client-entity * - * @property {string} buildDate - * @property {string} buildVersion - * @property {string} qtVersion + * @property {string} buildDate - The build date of Interface that is currently running. Read-only. + * @property {string} buildVersion - The build version of Interface that is currently running. Read-only. + * @property {string} qtVersion - The Qt version used in Interface that is currently running. Read-only. + * + * @example Report build information for the version of Interface currently running. + * print("HiFi build date: " + HifiAbout.buildDate); // 11 Feb 2019 + * print("HiFi version: " + HifiAbout.buildVersion); // 0.78.0 + * print("Qt version: " + HifiAbout.qtVersion); // 5.10.1 */ class AboutUtil : public QObject { @@ -43,8 +51,9 @@ public: public slots: /**jsdoc + * Display a Web page in an Interface browser window. * @function HifiAbout.openUrl - * @param {string} url + * @param {string} url - The URL of the Web page to display. */ void openUrl(const QString &url) const; private: From d717e803dd21ba02a3df688198741453a1ef039e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 15 Feb 2019 08:00:53 +1300 Subject: [PATCH 20/43] WalletScriptingInterface API JSDoc --- interface/src/commerce/Wallet.h | 23 ++++++ .../src/scripting/WalletScriptingInterface.h | 70 ++++++++++++++++--- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index c096713058..5e5e6c9b4f 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -54,6 +54,29 @@ public: void clear(); void getWalletStatus(); + + /**jsdoc + *

A WalletStatus may have one of the following values:

+ * + * + * + * + * + * + * + * + * + * + * + * + *
ValueMeaningDescription
0Not logged inThe user isn't logged in.
1Not set upThe user's wallet isn't set up.
2Pre-existingThere is a wallet present on the server but not one + * locally.
3ConflictingThere is a wallet present on the server plus one present locally, + * and they don't match.
4Not authenticatedThere is a wallet present locally but the user hasn't + * logged into it.
5ReadyThe wallet is ready for use.
+ *

Wallets used to be stored locally but now they're stored on the server, unless the computer once had a wallet stored + * locally in which case the wallet may be present in both places.

+ * @typedef {number} WalletScriptingInterface.WalletStatus + */ enum WalletStatus { WALLET_STATUS_NOT_LOGGED_IN = 0, WALLET_STATUS_NOT_SET_UP, diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 36ee021b29..c526f52765 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -1,4 +1,4 @@ - +// // WalletScriptingInterface.h // interface/src/scripting // @@ -30,13 +30,18 @@ public: }; /**jsdoc - * @namespace Wallet + * The WalletScriptingInterface API provides functions related to the user's wallet and verification of certified + * avatar entities. + * + * @namespace WalletScriptingInterface * * @hifi-interface * @hifi-client-entity * - * @property {number} walletStatus - * @property {bool} limitedCommerce + * @property {WalletScriptingInterface.WalletStatus} walletStatus - The status of the user's wallet. Read-only. + * @property {boolean} limitedCommerce - true if Interface is running in limited commerce mode. In limited commerce + * mode, certain Interface functionality is disabled, e.g., users can't buy non-free items from the Marketplace. The Oculus + * Store version of Interface runs in limited commerce mode. Read-only. */ class WalletScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -49,19 +54,56 @@ public: WalletScriptingInterface(); /**jsdoc + * Check and update the user's wallet status. * @function WalletScriptingInterface.refreshWalletStatus */ Q_INVOKABLE void refreshWalletStatus(); /**jsdoc + * Get the current status of the user's wallet. * @function WalletScriptingInterface.getWalletStatus - * @returns {number} + * @returns {WalletScriptingInterface.WalletStatus} + * @example Two ways to report your wallet status. + * print("Wallet status: " + WalletScriptingInterface.walletStatus); // Same value as next line. + * print("Wallet status: " + WalletScriptingInterface.getWalletStatus()); */ Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } /**jsdoc + * Check that a certified avatar entity is owned by the avatar whose entity it is. The result of the check is provided via + * the {@link WalletScriptingInterface.ownershipVerificationSuccess|ownershipVerificationSuccess} and + * {@link WalletScriptingInterface.ownershipVerificationFailed|ownershipVerificationFailed} signals.
+ * Warning: Neither of these signals fire if the entity is not an avatar entity or it's not a certified + * entity. * @function WalletScriptingInterface.proveAvatarEntityOwnershipVerification - * @param {Uuid} entityID + * @param {Uuid} entityID - The ID of the avatar entity to check. + * @example Check ownership of all nearby certified avatar entities. + * // Set up response handling. + * function ownershipSuccess(entityID) { + * print("Ownership test succeeded for: " + entityID); + * } + * function ownershipFailed(entityID) { + * print("Ownership test failed for: " + entityID); + * } + * WalletScriptingInterface.ownershipVerificationSuccess.connect(ownershipSuccess); + * WalletScriptingInterface.ownershipVerificationFailed.connect(ownershipFailed); + * + * // Check ownership of all nearby certified avatar entities. + * var entityIDs = Entities.findEntities(MyAvatar.position, 10); + * var i, length; + * for (i = 0, length = entityIDs.length; i < length; i++) { + * var properties = Entities.getEntityProperties(entityIDs[i], ["entityHostType", "certificateID"]); + * if (properties.entityHostType === "avatar" && properties.certificateID !== "") { + * print("Prove ownership of: " + entityIDs[i]); + * WalletScriptingInterface.proveAvatarEntityOwnershipVerification(entityIDs[i]); + * } + * } + * + * // Tidy up. + * Script.scriptEnding.connect(function () { + * WalletScriptingInterface.ownershipVerificationFailed.disconnect(ownershipFailed); + * WalletScriptingInterface.ownershipVerificationSuccess.disconnect(ownershipSuccess); + * }); */ Q_INVOKABLE void proveAvatarEntityOwnershipVerification(const QUuid& entityID); @@ -75,33 +117,45 @@ public: signals: /**jsdoc + * Triggered when the status of the user's wallet changes. * @function WalletScriptingInterface.walletStatusChanged * @returns {Signal} + * @example Report when your wallet status changes, e.g., when you log in and out. + * WalletScriptingInterface.walletStatusChanged.connect(function () { + * print("Wallet status changed to: " + WalletScriptingInterface.walletStatus"); + * }); */ void walletStatusChanged(); /**jsdoc + * Triggered when the user's limited commerce status changes. * @function WalletScriptingInterface.limitedCommerceChanged * @returns {Signal} */ void limitedCommerceChanged(); /**jsdoc + * Triggered when the user rezzes a certified entity but the user's wallet is not ready and so the certified location of the + * entity cannot be updated in the metaverse. * @function WalletScriptingInterface.walletNotSetup * @returns {Signal} */ void walletNotSetup(); /**jsdoc + * Triggered when a certified avatar entity's ownership check requested via + * {@link WalletScriptingInterface.proveAvatarEntityOwnershipVerification|proveAvatarEntityOwnershipVerification} succeeds. * @function WalletScriptingInterface.ownershipVerificationSuccess - * @param {Uuid} entityID + * @param {Uuid} entityID - The ID of the avatar entity checked. * @returns {Signal} */ void ownershipVerificationSuccess(const QUuid& entityID); /**jsdoc + * Triggered when a certified avatar entity's ownership check requested via + * {@link WalletScriptingInterface.proveAvatarEntityOwnershipVerification|proveAvatarEntityOwnershipVerification} fails. * @function WalletScriptingInterface.ownershipVerificationFailed - * @param {Uuid} entityID + * @param {Uuid} entityID - The ID of the avatar entity checked. * @returns {Signal} */ void ownershipVerificationFailed(const QUuid& entityID); From 4e4ec9940f1fb07d9da994354070a37830054c61 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 7 Feb 2019 18:16:08 -0800 Subject: [PATCH 21/43] copy external texture dependencies --- .../Assets/Editor/AvatarExporter.cs | 35 +++++++++++++++--- tools/unity-avatar-exporter/Assets/README.txt | 2 +- .../avatarExporter.unitypackage | Bin 13046 -> 13314 bytes 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 8f4d5a7962..2d0148b25e 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -14,13 +14,14 @@ using System.Collections.Generic; 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.1"; + static readonly string AVATAR_EXPORTER_VERSION = "0.2"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly string EMPTY_WARNING_TEXT = "None"; + // TODO: use regex static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { "2018.2.12f1", "2018.2.11f1", @@ -262,8 +263,7 @@ class AvatarExporter : MonoBehaviour { static string assetPath = ""; static string assetName = ""; static HumanDescription humanDescription; - - + [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { ExportSelectedAvatar(false); @@ -301,7 +301,7 @@ class AvatarExporter : MonoBehaviour { " the Rig section of it's Inspector window.", "Ok"); return; } - + humanDescription = modelImporter.humanDescription; SetUserBoneInformation(); @@ -479,6 +479,26 @@ class AvatarExporter : MonoBehaviour { } } + static void CopyExternalTextures(string texturesDirectory) { + List texturePaths = new List(); + + // build the list of all local asset paths for textures that Unity considers dependencies of the model + string[] dependencies = AssetDatabase.GetDependencies(assetPath); + foreach (string dependencyPath in dependencies) { + UnityEngine.Object textureObject = AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(Texture2D)); + if (textureObject != null) { + texturePaths.Add(dependencyPath); + } + } + + // copy the found dependency textures from the local asset folder to the textures folder in the target export project + foreach (string texturePath in texturePaths) { + string textureName = Path.GetFileName(texturePath); + string targetPath = texturesDirectory + "\\" + textureName; + File.Copy(texturePath, targetPath); + } + } + static void OnExportProjectWindowClose(string projectDirectory, string projectName, string warnings) { // copy the fbx from the Unity Assets folder to the project directory string exportModelPath = projectDirectory + assetName + ".fbx"; @@ -494,15 +514,18 @@ class AvatarExporter : MonoBehaviour { string exportFstPath = projectDirectory + "avatar.fst"; WriteFST(exportFstPath, projectName); + // copy any external texture files to the project's texture directory that are considered dependencies of the model + texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); + CopyExternalTextures(texturesDirectory); + // remove any double slashes in texture directory path, display success dialog with any // bone warnings previously mentioned, and suggest user to copy external textures over - texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); string successDialog = "Avatar successfully exported!\n\n"; if (warnings != EMPTY_WARNING_TEXT) { successDialog += "Warnings:\n" + warnings; } successDialog += "Note: If you are using any external textures with your model, " + - "please copy those textures to " + texturesDirectory; + "please ensure those textures are copied to " + texturesDirectory; EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index f02bc688ae..b81a620406 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.1 +Version 0.2 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 5e825bd0d97489f916b40aced9d764df74a7ae20..96a801496686bfb8536dc9fd97d65d8e68868791 100644 GIT binary patch literal 13314 zcmV+dH2upTiwFqU=3HC^0AX@tXmn+5a4vLVasccd*>>B?EzkZ6Mi0GqqsXFm#Z7x0 zTS?q{Nxd}Pa(rlsvf0R@DoSzOHec3M7L!VH;TAQv9zsAb%4u`M>`E z9?#$Z`nT=vAlUs90T~8gXX$(q&uhbMV|#l8KkUupXc3QstNUO-`jU)-H_`BOkWPa} zFE~!W1dV#wV;R%g{XDt8Sx{2&9)1RIlTkcL7WdWQU^=V`ko!rtm?u|D1iPG$;(4&R ziG$r)G=x8g$uOQ~aW(i=JkJuWr&g~~v9ma)jL~qI-p-=weKNfc#>oWA9PAB_&jvv{ z4{jEV*{kjCySuwuL;!2){Ca!BfM(k~=Su{7v$&mXY%DVbb1?-h2h;0h8t)iMqhyiJ zMbg=Qwuo=dx7xvpm9UpiCh>3qxM#I@@id+%!<~(d;RHI8MdsjDaFkBdH}MTnHeJqx zKW_x^Kx3D)5l}t&!f7yF-d;iWF*GoSwEN)0DUc#s1jCzXdL3u*l4L}SY7kAbG=MJ4 zVT>e@WEz|fcK444l#yxz(O)FP09ZUqr;~elpJPLIf3!4DvH7Yw))(c=fNdc?(2OrKZ-<_UZ9PeKq9UNc&21?dz^>Dnis?gbogX6*F z-uuDXIh8yceAqqRJ%`E}h&&k3xJ7WZ`|E?Fi=)enGr;-H2?7EX&Y@(Z-PtK>@Zjjf z`EM>i?w%eW9KX9fAN(2%RgTkX%o{8Ca(=RZ@+w$nF@U&^zZSs!-6x>--pSF?;CO$q ze|d3y05xzzo&oe}d?#}v-}zI)&QojD!p1nPRNdszpIrAQ_r0k-Z)(?@+VQ5gy{Rp4 zYSWvV(>iZzecW(T!&={$N^NsfyWZ4}H?{3eZFy6h-qeOSHT0&sZR^%rwUCSo@jpm6DD6(E@wXt-d`N;9-kcSU!I>_zCU<-aQX9-gX8ndB(A~=At;46&|(FC$l z;`?YiqV#570r(eG>1{kpK-)RR8o9+j%+s&Qt*55YQ{*sNEGDrBw-3qm^L;b7gn^t> zu8~_#V&|0cVRw{_MqcdLhh|QZ0`%DDMqWXMpIgX>;3-Iu2R%$~=4E$LQ-vBig(O;b z0UuK5H!!Mtu%h&4UV&V^=xZadn8eH};z4b2kC4Sr_xsy4#oi}>8#yF&y(TxuVR{$O zcjvbX&OC2d;=9LuEM-JIuXj5iCaSIh8$-aBAbTyD$%!j7ZL8^#SLU{%95yY<^^ejE(6!D=W1ECGJI*>ufh zCb)TpZW1r9a-uI%JISo@+n>!u&{a^(ev*ci`Hjk(mX_q`H`6VNuCjEpT%cDnj_2`o z2*1)rL_R3<8o?C~-Y;*XX_|m94~|X1eAeALxUA?qUVQ~0iaEW3=7W?qF@L#?KoqBt z`cv?G-VRO&$9sdz)06YvbLH?97xbE#R7~aBd$bb?e*CfnZ}nQg-)Z+-<1LM*+%|QAf=g5H_j*vakC2+RX1CvJb>f#{1Kz=jsdwtl zCcILu225tvx-bDqz18fuLMT;7px|0`o85YcVcP~*uF>zd0b3WOYYzJe0XX+M^>&w9 z+v?YQ^ym zwp))q^ySpi>oodJUPst%GqCj11V)NFfcGu4Ev|*0TSdLz?ywp{z`HGL=(n5oF0Y~2 zhN=r{SWPZqOS9W(aJIBtjdokIrQPdwMXjw?v(d`0sEA&S5A8;eSJDMYk_+8tt0U@Y zwELZoTL&9NSCFgM4ST&dr&puds#6v8(yrIBeJoeA*=y!iQHow6#C_YGUcE-QN6mUk zFO5#CA!_Y(>YaXmMXThi-R}2=47b8MR4Dk+>~tlE+Q5HPhFupXuak6|^^jN4?{->k z*-64Kah=Iv*zPp)Dkwp$dJlR^oAVt|Z_7T9Vk{fgZguP3ylPg7Rs-;F@Rrn}Mk!le zP$lxE-|B@fKU~Mkt)bHamJ?J6C-!!*rw(`v>Fw9Mpdxc?SWT=B0PZxHf(-k8 z==H<~tQ0f>Q4w=B;k9q^#C6?r+tLB~;ce;m+T8|kO1lmm2t`vmVQBRd>FPc8cqF_+ z7cK@+`!Ka8)L^p%T{&d9_nVDoTWG2}3{GLEZqh_~mi`XV?RQ&s-orwW#g5=(qucKY z$prQ`ZT)IGq<-Kqly{p5od0f!(XH8S0W;N0*Ba_fC)RIJfRHzP^_JjPy&1M!?Ov0c z5q%JK_8YWbvkT*tE0fCO_14hrwSZ%M>}j+?N`zdryWLJ#k_Py><@K2J@NOWPTTJV8 zL!3zKvETIuNMB!Y6U3*<`l{XKXvDE;{t9CY>4mm#SkBcolsd`tu|>k$^;4ss9rXT z;8fE>u1RP>8^%$3=|V-kewZ`Iu39GM;Hd&I?u%gyOj0SD^?DB!Jg=e$0tI$Ni3o*T zL$43xHkWq|Mc3&J|@s1An|#DhT5dtqydBwjfH_Wp)IkP`}+5WbF2P&Gfg%KMlnn6r#0vN-K4E}xY48h`#u!Cx(Ojyd(kPfa85r$SX)GVzw(2X|)+#;XE{xb|M8SKg#;QG*+ zdX2s`wOT#R)S?Q=(rVF&;eyUFwV0*V=r*OL1?o++w1iXG1;Oh1gtEZWV#XEgRN}X? zt=bK$i2rWp!Oj`;nVkj1ZFHK_&H~e+-B)&2qoGE8=oJkgCkresGPEG{(GZ4K7b;Me zR-@OEq;cG06#>jaJp|WB+F8xMW@mM}oRzH*?P(9(0y~SDRxQ#b{CA_NEGwcF|Ji8e zfG(goAcu|!LZMeshfU~*nvvC!rfs`nk6S80>B=|o62y&kZb(|P-Q$5cSWM6bSWOj) z>!HzWK_)6XQbWAX|>d$=6s#MrpZBIu0OHJRL=_pr&|X$Kl}J z<Yap9w5u4S-P5CWFj8Ger3n$WuyoQ; zE0x6R{qFJpI%rvTxBbB!gxt?+^iZEH`_%nn0(HN8p&Evq`^AFle9wcb3?4V#farYn z!&A0_??Wy`YcN*KroS;PMho7+48rLW+M&328pE=@+ZZ@8qFDsKeow(Y13V}3DG0er zJ5KkPGgzGm7#y)Ks(;(f?g8Rr4&NuxS61WzIbD$hMc2uK;d@MsM=Fi9o;Ptn&W5NX zri+s?mQDzG9AK7&8Y_QJO!CSF7H9%#Raa?JWOsAl|zcaK*0XCBh>C3k~6kIF`C1oO}>OQ z%7S=4Pv=7DacaYg2bisc^n_7qZqJMFFP`)Rm8`w%`N1YdK8y%Ica6b_@*fB zel*AlpXxVbMlRM?l}pja%4oD#_7Rn&XaAN*4giu)g7;BI4CP7|=>dIo zv7AqX=S;wA2ifs*GC7$KZfA@8O-Z1w9S-}O)hi5C0GwZLsSPBx_V+^={eGgu_$e%! zP9~#yEC}P~&|6$2DcHVmg&7!j)N3r+kQgHtI^q@`l(KUt&wVoZ(_pD3W~_~6%VNV- zjYX?LZ+O2wPX!##^K9ixQoRz3UW7YOC=x{%KcQw7hT{lUxsOCTN2{=ifG(?0;zgTa zKEO^$)>rd#AV1w4)Fp8Ao+mTvkT+HWx2%451h&-ytjE}_7`rF7*?s{q7Vh{4g38t> zy@D^O?O7(s!?NhQh<`haabH3_;u*%UQy}I|{x3))KnaG$5DKc~W_k?+jGn0!2?uD&AqhWB;wS^Bnh*oaj`f6Kk#ftfR#CLk zo^L@6;`daw1{paQW$V`8_yFY@{z=tq=m#tU zTZ0yFK|^8E$7FE>0y+4)fL%&BK+B3X07ofE>A@}EIdp5hy*9rh>|H9aK>`fST&CD? zoV-ZX&Qpp6Y;K`IRcf$UtP=d?FE$dOVPR-TNqRItrvX;NDk#ZRhmA(_3{-2;G+`s& z`TZ;oo?GPG)xXwAdq1*K3)Ye!vOvNP5I8nUnd?l!Yy|5*71|aD!9b{KNU&41{Gqmk zX*MI{5L~J0D7`C9I`PvPszB6CQra05l(Tr@wUPN~$9+Zi0Z_=6lLfcrnEyv3_;Ma| zC79`@hAGG>_@rDUUS3Vo;b)_ChP1tkHII`#NhH-RkQx%$r(i@}t7_0OGuuW7e(_{E ziXq{iIZrNjneLlH0jz+-w6r*t9S}OgMsUA>WRR>JNF@qXZhnd8mSqEV+P02QK`KXCcSuVm;6;US;ipImETW)KdqEh{~8h2hpC<% zHQ@I`R4caZMs9CoIb=OZ0sMajVxY;Cp)xB@|7njs!RiYO?gguUsqQVvr= z9fhlUEE!|JUJf>#;h*?6{Q}LKjvzBy-IHXB(e`@`{1jtXK8)L#RoJJd>EMt)2B1xq?_yeTrYE z?B)ARuxEI>h>~droo!Q}8S%@b2+O2n3Ls)cWtuKnu#p52;|KVIWu&oF0X5(W2Nl2w zq;?!xEI|Bxeh)?i^~z1LAO}nt1E5z>JwpQ(`al?qFB}&aVU<+?3SOI@@9%UJKGjsX zg~7DjsFyXHMLV^tdK5-r57gJsz-P4eF8*7l$ zkbHu{L|-7MWM3)+^RdL$DE4U$win=dikeuWx3yMTM_jCi*@J-kW!p8Qbhx|)Ap*|n4H@O#7W z$C<7_fSH_COqa;=%u(9?agYGuFN2iqUxO;-;1A(o4Dv6|Qx*jPq$u{X;}h)!=FXs* z-0R#==48L$*Qm;DQ#K7S`kCI)S9U{9=ytbFVWykTfj|p-lFk9bC>}@L24AEEI+$_7 z1Y-gT$dS;hsvxKE*?}{DZ!_*#TO;rZ;Dv7ShiDp4HY;ZgcFllV zsp?dMA$hEXTj!rqlOF6~%%tWs>}m8FMzC*c0>&l-tcqd0`NQ@P z)!>KU|Ne)qV&+WJ^z)JiaKQlz5d*^UWTfmUXg0N>klq|Z-bBMlZ46@0@JBOASqwvU1R}vE^zuYZZkF|X zEiQ#V$M?J%(~vZwZGpy5ZbghZ4+DY)WJZ2fsP6nnXN`k^-If)w(Vhk^>&aS@KW5@j+BK8D$A)(wSJY5|M4;5aq>j6H-Pqnk!z z9$mw@uOc81=;it9jPyHUt)hEUcO!qT{=!BfY_p0BzTlBHSCL`w69a%%AK4e8fNPhT z`#RZKf8;L2cXaGhG*!9ir%>ap8V_JG6oBXY1x{6ZiKh!iSP}!jpNYdv+YvO%i{Q!s z#U!knB{{(NL4%zqw=9awn)q>^ur#iNkgn*|$#geV>H=hqDh<)5u-{+s;rLT;MMcUk z0&5DU&&TRM1E8c|Aj&I+rnhK~1s8r-K# zaPFrIzl_vuA>nC1t5i$W^c%Ku`#8ma_h=w58YcwV;bbUo3$QY>KpEXos8PLEpQ^S_ z%MyOW7aF>Ov0M@R$#)j9NATU2Bv)uliewFnm8kVmVuqsxWPh3560ed4$X%JHD!~i> z1(K&^MH4L!7cdK6k{EIVFH%NaVRqzI35BVc7#XC&DGGA7Fo23%;Qhj2a0fEUVtKI~ z;SoyJrf_ckOfBPag0?EIGT~uA#niF?vF7X8bOogqj2P-EppBZ&F=lTfIkB_2pOMjl zCh!n{(aZrJ%>c6nHv!~GP&kpA!#K+%h`c(VcCOEUiYCTZ&x_^4wY1F{nL`l3Hmchy zi`H56CEkStRAMGPU;+mZV%Ih5>KUXW;#xDTB;s zW-5iEtVq&8SdNqmtKz8!wt37V=2AaBXr#-rZUpXtH@0YPOHCURN%7_liZDKvHhb0{ zc*LNyS|lD~=6r)rsuI+I!>A;g9stxB*@M%s##n+4x&zI6J&(z$lmkz!h85Lv2Ss5m zt&W43qmdeXbYXEuqjG-pLwsj!iN|f0c%elF`5q+B;#uKQ!#q1FjnZCqd2#wy6cb~@ zpiBxC{K^s#V-N>6=gOoYLd%XmYhcs?t`sekP%_#K6W}^>z?1-3qXIUTC83B@N zagmxFDG^;GiQ1?3*kQDPQqV>AkN=RMi_JB{9Vb^9XC?D-G+`EgIu=I0%LQ_i-kzPC z#IEY$x8i`Tn8t^lV?#9A_?$iY5vZKhn67pyxS=9Zn(+x^v26B0q>o{q|6ZKh{JgUB zR7(QPYG1r&o|2s4UvGN9Q1Z@LP0uS;vr@@@+jKzMs>QAOvngtrpsgfV>R3=~EQc??r+fD|En|-JE3~2`UGU^MYi6pU1sW?q z@f7~4Wxk4)ZU6knxc9f?{uR!&CQc;?JF zt>f(ID#;z1V;rdh-lxcNW?Ee~215YlTH}a-YL6uqBB#5imPJKMfM6LB!sThXXBO%|Wa6`GgxoSL z$ESEyVu13~09uWE^ZIOkAS@{Fx`rN)oaZD+iDH=^hn*mjO`%cHfmoj*q$nVDI1r_Jx&s?@UDPdom&+%BB~lqb7H9YB+b zphQN>8Bf}aAuZj z;4~Ef7!lPEnl#&=kQIQfN{F^TXd^Wuk0 zN>t%O^_n_X!fA8KEna|-I*H^KmmJR~_cju6;ULGCf02iHJRDE9&|cB)lgjjCyD{Y# z0zz}H@tQvng`BR5-H3LM$@TUx*$F7rv`n8W2BkBP;8sNadydGl%1)??m1le~NyBb1 zTP}pkQVW7z6}{fx6$bY~dfC>0 zge6@U-ZOAuQyL(FuFe7?!{r=d^AOh^-y?Jrk&ngt2%{HOSegLugF<-b z$rcQo=^VCt0y(lWmI^miN=|QC-N>hXiWT8Y?Q0E|q3N&p=IfuTfsy-e>)}SC;0FW6 z?LyWV+hW|rPX?Cwp&FE0_Gmw5`y?Q?hgIk;=PiTM zCCfA`_rL?2f+nk-Z?2$$`JrkPqf=y1eoZ%X!C8bFS9s?t|EIi04J&$OZP;1N< zPy>8{vS@4>&@qEB2U(w03*D44!e^Nx1+I$Fah3+UYKjVbBY$#+<8~Tn5NR34D`#iO zUpFRZT$uU{%{w3ouZfDPckEK?ZR%i)ovG;*&ExcfY)6X9nRj#%Yxs%)ZBb%^uJ(dXrrb1!9a(L(y0?^9~KV=1RLSeI}? zsf0Q)ZdH__*g!V66w5EUW7Ufnrsbg*_*B!+{qtL31$MRJ{t`i??`t+Fcl&t}YC6f~ z?+0x{*HF`{SZD9K`nBQl!t{CZ+~Z`ZgUH1@){&)joI(b}LN-q5?t41El4~^mfSb4G z!7H1&2Y~r?%el}cEE0|B#2Qz{U2ED+Wv;1Y<5V3UEKJ4>Z>h4__WLOm4~9%=QP^P} zG$`E(T6R_3*EIy7^N2ea>SJ0p}lhpcF2I?GDd4C^BBA8>COO(--*^|OBBT3axm zlFA86I%i9Oi!$yC%Zkeyfug-zNH(#%J^52i*bokovpC+RhXXkpJH;i;b6*V{OU5|# zNFGW&qopDq9_7Q1Tg8r`NCiLCDXbH*3~yCr)aJAh2=g%ryYg(?tK7k-AZn-st%-Kf z(58I>kZUkZy*O)Ob8(V2im_CE(ntbY|EOx00M-H#p4|nY+9jmeV&GoF^V_#;OkTL%Wdl{bJ?qqR~ z7Anqy6Vo|4#1C;MH{tYQ<#FA4?jWt|RQ4$f420S+eL6yY3xDr5)0If31LNigC#8Z! z63ADY%pg89n}fe94y1$s_~HwiA~$t_JazDj{~&#!-hQGEyw@%WK(!#UI0qJFfCu`3 zB7=8k4%}P?Ip?a0SE^Xy;fJS#y_2J(!SViJ|MKGa;QTk@5MMa_aCb)6QINB`)L?I+ zS@AOT;pUfDuATWCZ9G$PZB&Q+oK9+hIKd5a3xi{>+Q{-eFdT?ah1ZwWJWFI;WMImf zZ`#&x0)J>ck`N#p*=W-u1<}K(|6t1Y$)3N2egas*SGI4CUPmCDu=aBLsLGX^MZ0aB zTrcO0!hv;Tw@9O^bvkaH+>c$Z*DwyXJ=T57}e5NHyIgpd9DJn|oRoqn2e! zMM-YGbMF(a(|SzhN-8t7*Gydg8DivKjI)k7;jo~r6lHKMQj>j>P_9%xez8xXTjwm1 z$=$W=TrYx!-Gx1Rj^ZPlc+OIt!d#6ERm+nxWLw~8WhI^2ccq7F+-4}k)9jFj!KcPb zKz^l6TY!Ei3V-68!NjUX7o}28;ChM#E7H14`EIk$;ZM3xgi{o5eqxtO<;)?ffJq5DtJ~wVI#ZrELioF_F2o0w?pPx%X70(K zmik&El(nAy6C9v#D%wL$jTY!6V{4G|%-mBLth0RIc`C9Gmo*2U+~)D$N%;f{T-S)O8}Sgxqp-l68w;%i_`dEi3@{&QviNQUeF7$Gje$2d>OBfkbvLPvotloN z)rOFf8;|n>W@;yAtX3xc zMy!&>_I$1SVc+TbBW#pYge zPysrFi(uI=W%^F&E(VIq(<~&^L6UL?k)6Ti-uuDXIf={J;KS}I?U`7!FPYve4kiA4 z!emwXa#{IL=(L1oZm*@UCAzH-GPNrgGJP!pS)ar{PZa0shkPOQ&n#1`k~aW(RWt#=P6W)Zxq|Q!1>3+-M7{Z@8OTW;XMU3F76HbRIM^ol#~&Ih&I zw{Yqhl2rW9brecpXy|N3Ft_46+a%T#BM(J(%#t=?35|p!FFb%uKk=6{l2~ox=_=MD(gpnUMFAT zsT3z60L-XzIUj67aadbBx#un4+9Of&O=&#N^u9ay(AH#+e)?AQ_RlHb;i(f`aP87_9H; zpD3{E`?kG}lYA69+@_!*5)t)_oZ8E%dPs(p&#|-8PAr4Bu2W~5QjtTp_2(O>(`l`AvFaokKtY5B2kZvn&XkVL`ZNuj&GNm)iNx&6qR z0<4$VF?F)J-U3sJI;jn$18Ou*h_YRUqr{rN<_cFAdyRrpmpR@GSX;vhY;E@*<^77E zl*e8f7ihEGg7nVEgX8^^kCz`0_Rrr#Cu-O0JN_K+2M6!opF@^b$I0^M;{5#N`10)u z?0Y^t`0W64g&jN7;qIHk;fg$XFy-ajgTdjxnX#3hQ6biF@EV++o}8MH8g?FWCg$1R z>B-@tf);jla9ro`b2J%T!~ylrF^GaZd~;*vMX%<^LveJV%8+xQv6F-C4mqb)unuA> zqdM*wvvMdZ?$;1s;ZWdoGVyMWY2f9n^)0Q1v*i`6g{C_|uEC5JhcwfP%=;|yktZ`# zWj4qlW$$|s3%`nnpB;=CB6QTedwsK0N!5Wr)S>G->o%F5!S+v!EPC&$nmtAJQ;c#v zxa=ZXOkx})aC@a#@$B5tpgbg3u5=2>Y^F%KXS>>jhCBvoRK~NL^p2mxX>PU`GV$)> zK7iOYiuyPtlE^P`me1IC2Pz(cc>E zH1!s^gKc>q=XmA`-2~+}K*+qe2?~&gz9_K*qkA|^Aid)i>eU`bu!aq<&BsHp!+Oqc z!w>564lZv#V1Ybq2XP=`aVitpBc zR*kss|EKD>_fNLAd*^>X2N~d%m-(-X-kioGra7485YK^$;y8HRyG3XCzMRLf z5mRK&3S<+%qa^BTBsaUX;&}eYcd*}(1`_Hjr&Sr6d+=!@Pv#M4@5qL#1Mc(N`jKaW z=8EiA19I(4O?YECQjC57RTAHAdP`Kb0w0sn;>NI`w#r-%ock!qYz*A=dIehrT-59- zhY)TV*nzDz_fgDdija4A3>6P==;A%xMVyT9tt*1DfAMowCij|orGtcIu*aA!46QKk zn_{6OD2yM-FkGa}z~sj~aV=q-?ZeZ6<>Lrqt~)DK7;90n3R57}=x$7YP*Cq}Iv-G9 z-NanpkP2D3ItOwL;7!+yF@{Es|7)rrcI&5L;2)ORf+){s2MprV${gD5!jW1*NCvrp zkct2+k0<$vV^W;pRvB%4=C|%6zLektfE89_9O8pg8m&uq?R2u|B)Ae@ln z=n9Tmu-jFmYWQm0*n4HD#cvR_{ENecT-t>+Sp|%s28{;A&Ng>yqUD5iEJg&_Bmi$K z+teXpxvsrEy2-nhbSdMBX8+N?n?tO1k}MjyQW@L=Gx+JfFprh|aT#;{faapO!Es^KV#?2U&R#l5Elc9GGCSOuC(+y5YT2N`M;eArn7uFu8^ADHj+@V^fO7nuKnQUd=e-jR@*Q z207N{22INPy2+DZ^iUZgtk$}tI``5sMsXG&WJnp9UIOYj(KdC+91k{r|~* z>h)H=IUWt$jj-2kgsow(751)%^{6{;_PTLnn+`Fq!DMyx?4eIRtk=8UF4RSj{kPui zG@pgdR@kaT&Q=KdTVdFF7Sx}8hi8d{)wB9{sq5eH!BE3nTD2rj(Fz(H*UMz|Dp(~N z8%OChJ-DSS;pVU|RbI8m&Qr}^1%Jj)3UkoxFw2ovLm&*;)0mXRFox{E`$RzCXhJ$U zA5S_1EFdizVyQnT{2t(geMkn-w0Hy$6n}*pfCTjo7{2VjQ9JEd5ugFPB!d5|gSGlU z#tCt3efIEYRsXLyyV(C5-LTVaHoB0%+3AM=@BiQ9VHeGAvzY8Qo8KFJWqrCf%%1)K z`R{(}jW7&{?Rwk|N25-sHHv!Cs6FgNop=~^JKgPPkAFbN`1`+ltp49BoxHiiPw!q zi7h`U`S;As?yO%?gA@@|@SNhTy*r-yyk76TI_tlSk^VOd=4*ew@O>|d=db-BRO|V2 z6UK}9%D;*OU;U-^pTAs=>%Vi*QT@}K_~IlwGXDpm`Th@7r#Iv=wNmT7kflDgx{&ZLp&r1M}`svec1 zWb#WHN^hBXXkuz7mp5??&|Pn6iNmd|(J3Okazy8_S>cWAcv8@slk)Zz0;~P=NmOS9 zUGFn$)4cJeFG^jM5P00PyFpw;_xCzBG=_sr2mC-2d1X;gcw{bQUx}>1E-0lww}4D) z@fH|syr-!FZpoKId%0mxZFZaFgDG96qdm$><-&ts!-2`X$1M<&BueewX5!&MpJh&# zA7&J;)xGRe6VSTa=V-07F(ud|TXC*2MZXlT6G-tDB5Z`UQ*z?+oI4JoBO^=k+ z;vxM;nl98-Gp?cd?g{mL(cImbA&ri7fk^cI#QqE@%8H(l!FYU&l2CM)SPbz4)p{@u z|3=&(VSL_AF^bN;4?D0R?1KbU`6Jz^Db7a`DT)z;r+m)|w~6_`+>(H_k0*^toX|*( zlL#?I|FO$Nt_qC(Hd94&$6*^T^!Ev9x#aL6ap^k4N*=qt{n{p{sk)%r@)$=>4wuc` zNR#3OCBT`X_SDfP%TpEGIgWc_ec1X%^n}rg2i*V?V50F@=uFylAGa9@uP*d-t12BU zxZ6C`G0CsUAh<@AfXa%FDMwYQh)=i+sU@7AP4nq1`Va$0+17fV+k*( znPztipq(1`=$YoGRG0AX@tXmn+5a4vLVasccd*>>B?EzkZ6UOlvS1$%kOr(@RuIrulOBAtuW|DomQ*;%nyRVZ$IA=fo z-t~Nb`*LgR?(VJ`6ToIRyV{yCp!pUr`2vAn&u=DcYl|GgoKFGE!SpJfCfi2RD4l0B zk#u^W&yySTt+{_(NZ8FLlVmst-1Fw!WSY#<;r80vZ~_CVKxXfycbHAH*U2?dHe1ZR zzpQ!iKxY@XBcQtXnbTmpxVePlW9VQ4Y4_gy6Cg!A_lDQ;^eV~WCC!Nzn_fK0GY^I= zhcS{s(y4bc*x5TAP(f-5M1P(RJz()Dn@;ZGeTE&``Ss4(&dJ5#H}8*6&ITtJzaE^N z?jIj{ue^re3>w>1*9l-e$pEW&`|nRL-kuzvAMITn?jK$J7HaxUKNxQ>t91H)|7dWr z`)+W0Mm0|d?{`ji&Y*Dyq6`LfZtflK{AT~~{P5!Z6mWihjDP@zGpHH1JKI$q9vr?u z`|ZVtos*;eqqi4lgWq7K#!)s+cxNjr|NbM8b@%x2aB#FY*tDF3yfG-tE8Hzxc)R{?XaR(azz3Nn}-JC8K|7>|Q7Nys_yuPH#aq z292+qTIM^z1oG%_At$}fA&35&IS0vj?(Kup|3cG1Jb_}A_%5D~C_O5x0RMu{yh%nW zs5PrvSX%AyomIz$-C;T!Ik96I zQAw2w^jLOSR#D++S8^eE0utmv50j&^;x=lkQdm++qE)QmLh9@qrcVb}lpd8;D8-9% z!?J1;GpmXNwZT0?7C-I$H(7?GPyRMaNa%V?c8P=RE}8AjZWNqZ0p>~uEKS4JAHFHkD4z)B6 zOI0eTw2C1FcEzx?q7(zWQeaoI^$tr`il@Ae8%3ph%4Q|Ta8MJb`O3grl5t^>V-9EHUra(|nM+Cg!geF^J+6Qhx@1&zr%?;AnSnadLdN zbEX`g>WWSild7pQ>o36D>xHdOI~s3#U~9L-ZYz1wg_jqdX1CpK_mdYLc=Mb6ey81U zjW;x!a@*7f3NDS`@AaT=A0b7}sM~L~I?0P5gm-Xa{Ei<*@Jg*3Fqu_r!vr9HE9$ob zsO2M2a4ot~*Y7ZF+u+KD{canuwL#kEu!|6YbFbsKyVToO-|zXo9yWw8K^XS?^nyL} zBgnB@aLr|QI|MFj^+SY>&^i!<3kB#-EAo*htW>8Lwe5C_Q(s9Ny-wJVcpE{t&A`%2 z1dJ4I0PkC7Uu+A#v?Y3k~ zyVvWAR$HwoY?U`uMK8vOcG%;MbODm&LN{u4L>pnd-|5(Gut{_Yxq988*K2cng;C3= zCg`Q@``ABLDvEkhSrfJB6?B76o71Zoc6-#V7xWTB;8(QT>G+*~c|*(OtKIJRg$%b~ z9a+GIQQGNB4z+>*rVQIIOxYmmM1H^<=yyA+~< z>TNmZQH*7$+891BYi5~fg@Av^d*VZjQntFFO5{tw)eCHX*p8LmLZ<^P2dLOW*y{o5 zIIW_N4|ogd?fYF&k)hg=Q~5AC z1s&g{iSjJn1EAaQwtPOq0+7Xy;A7bBcZ6gDd!wR$H62nnaG1)w5d!DGLovHW-4-xY zy>tsxo$1874GIwQsOPr?xBMt*x7xjkn-P5wb@m&yUetwo%9csx@j6?GdM)4>pL@bK zNQsb(cDLKTobS2S)J`ErK zJLpI{cSFCWUV?t)23|?@P6t$dfJm~}UZ(}>j8Pq*4yiUq_9#LNWW)0ApL+1vaRIvp zHpTb)Vu}xfPM|EWR+}^%6#@kWRIeLFu$pNh*CaHc4dW=ibfF>MeiVf`RO`eX98Cbm zeKBo;Nh(Ft_j{n=c@sSlD6k`HL@4YQdVQF;x$MKh3L?>ozz1;^^L{I8cY(pR;~6fV zw1Tdn3AUrGu2vX{nIE6=My=q7-2`-`D=Q8{8C^YiII&>lplee*u44Bj=yYL;pM#4cn4qN1)n(2V6+#1lyiqF%;}ES5428BJ zO3-C?1fx*D-4|r+_Iof2mqu({qa%X70UT97gjr3wB^{D!l0Hy`z>S +y}Fp9RlB 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 Date: Thu, 14 Feb 2019 13:41:54 -0800 Subject: [PATCH 22/43] Updated manual to include Android. --- tools/nitpick/ConnectedDevice.PNG | Bin 0 -> 1859 bytes tools/nitpick/Create.PNG | Bin 16996 -> 21035 bytes tools/nitpick/Evaluate.PNG | Bin 13435 -> 14297 bytes tools/nitpick/README.md | 597 ++++++++++++++++-------------- tools/nitpick/Run.PNG | Bin 20675 -> 0 bytes tools/nitpick/TestOnDesktop.PNG | Bin 0 -> 25162 bytes tools/nitpick/TestOnMobile.PNG | Bin 0 -> 18623 bytes tools/nitpick/Web Interface.PNG | Bin 12712 -> 0 bytes tools/nitpick/WebInterface.PNG | Bin 17033 -> 17929 bytes tools/nitpick/Windows.PNG | Bin 14082 -> 18164 bytes tools/nitpick/setup_7z.PNG | Bin 4104 -> 0 bytes 11 files changed, 326 insertions(+), 271 deletions(-) create mode 100644 tools/nitpick/ConnectedDevice.PNG delete mode 100644 tools/nitpick/Run.PNG create mode 100644 tools/nitpick/TestOnDesktop.PNG create mode 100644 tools/nitpick/TestOnMobile.PNG delete mode 100644 tools/nitpick/Web Interface.PNG delete mode 100644 tools/nitpick/setup_7z.PNG diff --git a/tools/nitpick/ConnectedDevice.PNG b/tools/nitpick/ConnectedDevice.PNG new file mode 100644 index 0000000000000000000000000000000000000000..81d567cb6b9c728e26859cbb09e3488bbd88fe3d GIT binary patch literal 1859 zcmV-J2fX-+P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2G~hNK~#8N?V7=r z>N*UDsp+z#i@T+Ov_T6L@D?;MTUK{N9kZHB@+`-)Wm!rx%mEYT|IX0?E4Hk7V*N>m z-3R~x0CWao1ONb_I}jrP007;A7y$qP=uRU>A0Hnu*wYOF@Vha^=)n~1F zbG(5;i~s;x>Iud3H*w^Am+_eW)SvPx?fvOi=b}YQ`m8ykPYNr|7ur=Uor4oA$9Qdn z7y$sbCq_-NmNpe>7L@n|MX&Etq0T}>ulT62dwv!Gj+=#G5}w?@IljivvAsFyDNp6Z z=iuhx$0~hZ*C0j!fNhGAS7`MyYEzN6{M4TF=cf!dBaR-o%AbV8n}hnW0EO?Dqj+NC z+;q&jSQ{{i5ddI=uj#0mJFlU1p^ioRPBD^xef%f>r8%O}(+`PBI9Kj7H*Pl8>l?)A zMfp6Bhr@aNOwCo?GlucTlAmv~hHG*@nV+23Z@^if&+{SO93RH>!s5Nz4;V{f*L2pCh>l;y_2!t2azBQ1%KwAPvAoxGeymrwhl!CaV!;keyWv&v z@~-^LoBPMyQ-JRo!+2vG|8*SlIt-ze@YcukFB0bAc~_p-bqI5$ex(o#i()GCXCJZB z&*xB`m%P5$#1ZQzkoo&@3c3I6`h1S(!<&i`*(69}q4=lWQ5e{t1VW1fw>>|9m+>7x z3jqC}huX~#7)0OZhXvjo`IrzmYR}~`7wg6CQDQVyg(qw*V>7)TQ~<2J)rP(fqG$+K z7R^=M6YD4Qdt)mHxdPD2E8P8AXW86-kO}?@}eb0Hz{3u^B z?msbx3Z8ivug*h^H-$px(X0=*z4h_e&AbjIa^Cp8TpzkW#fm~x& zk$454$Lbh$doll|{O>B$8uqw4w&bVpNcZm+O@CCEWgZO$4NBv>Nx(o z&FA0dWA>j}pDBj3?gEGrth+r*j51MN!iFl!AHaJVtY?by9i7<5pZ9kFfgbSi$YBkGpOvkt|Igp?k9*EE#>=k&F@klshl^2!HdELf zag&IRL2cSZ{3(6#FymO~ zOL+dkY#cp~+If8-MzD?8qr~VScaF1Z^6LcYx4wtZp;RtW&`LovV-dNn3Wx4Tkk86H zjupj#{40xLMvYm_H~Z_k#v9112pXb386VD$O(l{^vF0QA98z&>zO(F7MjuYViV%+4G>jN=@ZNwfW zM)m82vKgpIvfunQkokQoYI$4=7B}5oEJC*V)f=1a7nur0=_58V#j*SnJ-6ap`AUB^ zwtH$*hLm+8i{~ZUrf3pX zu1?Wc*Fdh>o?opU`*n_Fajq5do17S#zZXZs{Z)?j`aq0e8?i@;(aS@d-Q3}zqAKuU zdq9j}8?oOcM)GdFdcpuc@~#`=y(!Am+nr(rgFV{-0Kc0?i~s-tfM)|S0ssKe9f%PC x0D$g5i~s-tbO&Mt005vn5F-Ep0G%1f{{fFCIggxoGEM*h002ovPDHLkV1iO`kskm6 literal 0 HcmV?d00001 diff --git a/tools/nitpick/Create.PNG b/tools/nitpick/Create.PNG index 85d70e59edadb70f9d0dc4d26e9683fa0e69701a..d869a0b80758adab4e60266cb6a62be17cdb309f 100644 GIT binary patch literal 21035 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 JK%Tn#{{ULbKKcLv literal 16996 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 cae*QJ_NuWk?H*la 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 diff --git a/tools/nitpick/Windows.PNG b/tools/nitpick/Windows.PNG index 32e3bfd53e90e1394317e678718ab1f1ae991d1b..a74261ce3972260c3ac286d105e7116a3b280cd2 100644 GIT binary patch literal 18164 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 L_Wn0V|MUL<*a7B0 literal 14082 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 a26Enq2hU1eKp+q(YTM^qssB9u^?w7pP<-70 diff --git a/tools/nitpick/setup_7z.PNG b/tools/nitpick/setup_7z.PNG deleted file mode 100644 index aae4123cdf2661cd7ca185cae3f0b33e0b6feca0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4104 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 Date: Thu, 14 Feb 2019 13:46:51 -0800 Subject: [PATCH 23/43] Completed manual. --- tools/nitpick/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index e4b6b15dda..67099ae190 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -221,7 +221,11 @@ Leaving the Run Latest checkbox checked will download the latest APK. Uncheckin Clicking the Download APK button will set the status to *Downloading installer*; this status will change to *Installer Download complete" when the download has completed. The APK will be located in the working folder. ### Installing APK After download it is possible to install the APK on the selected device. -When installation completes, the status will be *Instalaltion complete* +When installation completes, the status will be *Installation complete* +### Run Interface +Pressing this button will run the full test suite on the device. Snapshots will be stored on the device in */sdcard/DCIM/TEST. +### Pull Folder +This button is used to copy the snapshots from a test to the local device for evaluation. The default is the default snapshot folder on the device. ## Evaluate ![](./Evaluate.PNG) From a1e289cc1ce67497c93a88dfaae079b8a783ab62 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 14 Feb 2019 14:17:29 -0800 Subject: [PATCH 24/43] Compilation error on Qt 5.12 --- tools/nitpick/src/AWSInterface.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index 63d5af9272..4e83460b9e 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -589,4 +590,4 @@ void AWSInterface::updateAWS() { QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename; process->start("sh", parameters); #endif -} \ No newline at end of file +} From 01def37efd2d5e63598fbfa37a6e4c7d193cfaf1 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Thu, 14 Feb 2019 14:22:43 -0800 Subject: [PATCH 25/43] spacing --- libraries/fbx/src/GLTFSerializer.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index da77ecd77b..736e7831c1 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -900,16 +900,16 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { tangents, accessor.type, accessor.componentType); - if (!success) { - qWarning(modelformat) << "There was a problem reading glTF TANGENT data for model " << _url; - continue; - } - int stride = (accessor.type == GLTFAccessorType::VEC4) ? 4 : 3; - for (int n = 0; n < tangents.size() - 3; n += stride) { - float tanW = stride == 4 ? tangents[n + 3] : 1; - mesh.tangents.push_back(glm::vec3(tanW * tangents[n], tangents[n + 1], tangents[n + 2])); - } - } else if (key == "TEXCOORD_0") { + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF TANGENT data for model " << _url; + continue; + } + int stride = (accessor.type == GLTFAccessorType::VEC4) ? 4 : 3; + for (int n = 0; n < tangents.size() - 3; n += stride) { + float tanW = stride == 4 ? tangents[n + 3] : 1; + mesh.tangents.push_back(glm::vec3(tanW * tangents[n], tangents[n + 1], tangents[n + 2])); + } + } else if (key == "TEXCOORD_0") { QVector texcoords; success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, From cf2e118c14c0663b84f1a97e4d3e983452216ef2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 14 Feb 2019 14:33:49 -0800 Subject: [PATCH 26/43] Added missing double-quote. --- tools/nitpick/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index 67099ae190..c7b9050070 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -29,8 +29,8 @@ Note that X.X.X is the latest version. 1. Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vX.X.X.exe: aws s3 cp nitpick-installer-v1.2.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-vX.X.X.exe #### Mac These steps assume the hifi repository has been cloned to `~/hifi`. -1. (first time) Install brew - In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` +1. (first time) Install brew + In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 1. (First time) install create-dmg: In a terminal: `brew install create-dmg` 1. In a terminal: cd to the `build/tools/nitpick/Release` folder From b94655260dd7b1d66bcde5dcb30f490da675877e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 14 Feb 2019 14:54:10 -0800 Subject: [PATCH 27/43] don't traverse for fullUpdate on changed view --- assignment-client/src/octree/OctreeSendThread.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index ab357f4146..af36df1eba 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -324,12 +324,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* if (isFullScene) { // we're forcing a full scene, clear the force in OctreeQueryNode so we don't force it next time again nodeData->setShouldForceFullScene(false); - } else { - // we aren't forcing a full scene, check if something else suggests we should - isFullScene = nodeData->haveJSONParametersChanged() || - (nodeData->hasConicalViews() && - (nodeData->getViewFrustumJustStoppedChanging() || - nodeData->hasLodChanged())); } if (nodeData->isPacketWaiting()) { From 98a5ec84a9ac163c18593503aa5282b0a1708d2d Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 14 Feb 2019 15:35:02 -0800 Subject: [PATCH 28/43] Adjusting the HUD operator render job to shave an extra framebuffer --- libraries/gpu/src/gpu/State.h | 2 +- libraries/render-utils/src/RenderCommonTask.cpp | 5 ++++- libraries/render-utils/src/RenderCommonTask.h | 8 +++++--- libraries/render-utils/src/RenderDeferredTask.cpp | 2 +- libraries/render-utils/src/RenderForwardTask.cpp | 12 ++++++------ 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/libraries/gpu/src/gpu/State.h b/libraries/gpu/src/gpu/State.h index abe0cd7731..2e8a3f2cab 100755 --- a/libraries/gpu/src/gpu/State.h +++ b/libraries/gpu/src/gpu/State.h @@ -246,7 +246,7 @@ public: struct Flags { Flags() : - frontFaceClockwise(false), depthClampEnable(false), scissorEnable(false), multisampleEnable(false), + frontFaceClockwise(false), depthClampEnable(false), scissorEnable(false), multisampleEnable(true), antialisedLineEnable(true), alphaToCoverageEnable(false), _spare1(0) {} bool frontFaceClockwise : 1; bool depthClampEnable : 1; diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index b4a77479db..9a70b1c1b3 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -101,7 +101,7 @@ void DrawOverlay3D::run(const RenderContextPointer& renderContext, const Inputs& } } -void CompositeHUD::run(const RenderContextPointer& renderContext) { +void CompositeHUD::run(const RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputs) { assert(renderContext->args); assert(renderContext->args->_context); @@ -119,6 +119,9 @@ void CompositeHUD::run(const RenderContextPointer& renderContext) { renderContext->args->getViewFrustum().evalViewTransform(viewMat); batch.setProjectionTransform(projMat); batch.setViewTransform(viewMat, true); + if (inputs) { + batch.setFramebuffer(inputs); + } if (renderContext->args->_hudOperator) { renderContext->args->_hudOperator(batch, renderContext->args->_hudTexture, renderContext->args->_renderMode == RenderArgs::RenderMode::MIRROR_RENDER_MODE); } diff --git a/libraries/render-utils/src/RenderCommonTask.h b/libraries/render-utils/src/RenderCommonTask.h index a1de50abba..bc1031a18c 100644 --- a/libraries/render-utils/src/RenderCommonTask.h +++ b/libraries/render-utils/src/RenderCommonTask.h @@ -77,10 +77,12 @@ protected: class CompositeHUD { public: - using JobModel = render::Job::Model; + // IF specified the input Framebuffer is actively set by the batch of this job before calling the HUDOperator. + // If not, the current Framebuffer is left unchanged. + //using Inputs = gpu::FramebufferPointer; + using JobModel = render::Job::ModelI; - CompositeHUD() {} - void run(const render::RenderContextPointer& renderContext); + void run(const render::RenderContextPointer& renderContext, const gpu::FramebufferPointer& inputs); }; class Blit { diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index a685f3998e..44b38c0d8d 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -258,7 +258,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren const auto primaryFramebuffer = task.addJob("PrimaryBufferUpscale", scaledPrimaryFramebuffer); // Composite the HUD and HUD overlays - task.addJob("HUD"); + task.addJob("HUD", primaryFramebuffer); const auto nullJitter = Varying(glm::vec2(0.0f, 0.0f)); const auto overlayHUDOpaquesInputs = DrawOverlay3D::Inputs(overlaysHUDOpaque, lightingModel, nullJitter).asVarying(); diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index c47935c6fc..850de8eae9 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -130,20 +130,20 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend } // Just resolve the msaa -/* const auto resolveInputs = + const auto resolveInputs = ResolveFramebuffer::Inputs(framebuffer, static_cast(nullptr)).asVarying(); - auto resolvedFramebuffer = task.addJob("Resolve", resolveInputs); */ - auto resolvedFramebuffer = task.addJob("Resolve", framebuffer); + const auto resolvedFramebuffer = task.addJob("Resolve", resolveInputs); + //auto resolvedFramebuffer = task.addJob("Resolve", framebuffer); // Lighting Buffer ready for tone mapping // Forward rendering on GLES doesn't support tonemapping to and from the same FBO, so we specify // the output FBO as null, which causes the tonemapping to target the blit framebuffer - const auto toneMappingInputs = ToneMappingDeferred::Inputs(resolvedFramebuffer, static_cast(nullptr)).asVarying(); - task.addJob("ToneMapping", toneMappingInputs); + //const auto toneMappingInputs = ToneMappingDeferred::Inputs(resolvedFramebuffer, static_cast(nullptr)).asVarying(); + //task.addJob("ToneMapping", toneMappingInputs); // Layered Overlays // Composite the HUD and HUD overlays - task.addJob("HUD"); + task.addJob("HUD", resolvedFramebuffer); // Disable blit because we do tonemapping and compositing directly to the blit FBO // Blit! From 522a9ed7c1acfc14220a2bd32fe8c3c8cd53acb7 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 14 Feb 2019 15:43:36 -0800 Subject: [PATCH 29/43] Keeping only the minimum for quest --- libraries/render-utils/src/RenderForwardTask.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 850de8eae9..95bd26e165 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -135,11 +135,14 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend const auto resolvedFramebuffer = task.addJob("Resolve", resolveInputs); //auto resolvedFramebuffer = task.addJob("Resolve", framebuffer); +#if defined(Q_OS_ANDROID) +#else // Lighting Buffer ready for tone mapping // Forward rendering on GLES doesn't support tonemapping to and from the same FBO, so we specify // the output FBO as null, which causes the tonemapping to target the blit framebuffer - //const auto toneMappingInputs = ToneMappingDeferred::Inputs(resolvedFramebuffer, static_cast(nullptr)).asVarying(); - //task.addJob("ToneMapping", toneMappingInputs); + const auto toneMappingInputs = ToneMappingDeferred::Inputs(resolvedFramebuffer, static_cast(nullptr)).asVarying(); + task.addJob("ToneMapping", toneMappingInputs); +#endif // Layered Overlays // Composite the HUD and HUD overlays From e800a6e0306515993b4c5573dcf5bee00b4266fd Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 14 Feb 2019 11:59:04 -0800 Subject: [PATCH 30/43] Update hard-coded vec3 zero with Vec3.ZERO --- scripts/system/libraries/entitySelectionTool.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 3e16315c6d..13c14f2010 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -284,9 +284,11 @@ SelectionManager = (function() { properties.parentJointIndex = null; properties.localPosition = properties.position; properties.localRotation = properties.rotation; - properties.velocity = { x: 0, y: 0, z: 0 }; - properties.angularVelocity = { x: 0, y: 0, z: 0 }; } + + properties.localVelocity = Vec3.ZERO; + properties.localAngularVelocity = Vec3.ZERO; + delete properties.actionData; var newEntityID = Entities.addEntity(properties); From 7cffcf221595a83f58d28c9207f1eec98dd5f298 Mon Sep 17 00:00:00 2001 From: David Back Date: Thu, 14 Feb 2019 16:09:48 -0800 Subject: [PATCH 31/43] error handling, warnings for duplicate textures --- .../Assets/Editor/AvatarExporter.cs | 166 +++++++++++------- .../avatarExporter.unitypackage | Bin 13314 -> 13655 bytes 2 files changed, 106 insertions(+), 60 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 2d0148b25e..624b365c17 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -263,6 +263,7 @@ class AvatarExporter : MonoBehaviour { static string assetPath = ""; static string assetName = ""; static HumanDescription humanDescription; + static Dictionary dependencyTextures = new Dictionary(); [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { @@ -304,51 +305,38 @@ class AvatarExporter : MonoBehaviour { humanDescription = modelImporter.humanDescription; SetUserBoneInformation(); + string textureWarnings = SetTextureDependencies(); + + // generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar + SetFailedBoneRules(); + + // check if we should be substituting a bone for a missing UpperChest mapping + AdjustUpperChestMapping(); // 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 as warnings in the dialog + // and also include any other bone rule failures plus texture warnings as warnings in the dialog string boneErrors = ""; - string boneWarnings = ""; + string warnings = ""; foreach (var failedBoneRule in failedBoneRules) { if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) { boneErrors += failedBoneRule.Value + "\n\n"; } else { - boneWarnings += failedBoneRule.Value + "\n\n"; + warnings += failedBoneRule.Value + "\n\n"; } } + warnings += textureWarnings; 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(boneWarnings)) { + if (!string.IsNullOrEmpty(warnings)) { boneErrors = "Errors:\n\n" + boneErrors; - boneErrors += "Warnings:\n\n" + boneWarnings; + boneErrors += "Warnings:\n\n" + warnings; } // remove ending newlines from the last rule failure string that was added above boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n")); EditorUtility.DisplayDialog("Error", boneErrors, "Ok"); return; } - - if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) { - // if parent of Neck is not Chest then map the parent to UpperChest - string neckUserBone; - if (humanoidToUserBoneMappings.TryGetValue("Neck", out neckUserBone)) { - UserBoneInformation neckParentBoneInfo; - string neckParentUserBone = userBoneInfos[neckUserBone].parentName; - if (userBoneInfos.TryGetValue(neckParentUserBone, out neckParentBoneInfo) && !neckParentBoneInfo.HasHumanMapping()) { - neckParentBoneInfo.humanName = "UpperChest"; - humanoidToUserBoneMappings.Add("UpperChest", neckParentUserBone); - } - } - // if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest - string chestUserBone; - if (!humanoidToUserBoneMappings.ContainsKey("UpperChest") && - humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { - userBoneInfos[chestUserBone].humanName = "UpperChest"; - humanoidToUserBoneMappings.Remove("Chest"); - humanoidToUserBoneMappings.Add("UpperChest", chestUserBone); - } - } string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string hifiFolder = documentsFolder + "\\High Fidelity Projects"; @@ -407,7 +395,13 @@ class AvatarExporter : MonoBehaviour { 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 - File.Copy(exportModelPath, assetPath, true); + 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 @@ -455,12 +449,21 @@ class AvatarExporter : MonoBehaviour { } // write out a new fst file in place of the old file - WriteFST(exportFstPath, projectName); + if (!WriteFST(exportFstPath, projectName)) { + return; + } + + // copy any external texture files to the project's texture directory that are + // considered dependencies of the model, and overwrite any existing ones + string texturesDirectory = Path.GetDirectoryName(exportFstPath) + "\\textures"; + if (!CopyExternalTextures(texturesDirectory, true)) { + return; + } // display success dialog with any bone rule warnings string successDialog = "Avatar successfully updated!"; - if (!string.IsNullOrEmpty(boneWarnings)) { - successDialog += "\n\nWarnings:\n" + boneWarnings; + if (!string.IsNullOrEmpty(warnings)) { + successDialog += "\n\nWarnings:\n" + warnings; } EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } else { // Export New Avatar menu option @@ -469,33 +472,13 @@ class AvatarExporter : MonoBehaviour { Directory.CreateDirectory(hifiFolder); } - if (string.IsNullOrEmpty(boneWarnings)) { - boneWarnings = EMPTY_WARNING_TEXT; + if (string.IsNullOrEmpty(warnings)) { + warnings = EMPTY_WARNING_TEXT; } // open a popup window to enter new export project name and project location ExportProjectWindow window = ScriptableObject.CreateInstance(); - window.Init(hifiFolder, boneWarnings, OnExportProjectWindowClose); - } - } - - static void CopyExternalTextures(string texturesDirectory) { - List texturePaths = new List(); - - // build the list of all local asset paths for textures that Unity considers dependencies of the model - string[] dependencies = AssetDatabase.GetDependencies(assetPath); - foreach (string dependencyPath in dependencies) { - UnityEngine.Object textureObject = AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(Texture2D)); - if (textureObject != null) { - texturePaths.Add(dependencyPath); - } - } - - // copy the found dependency textures from the local asset folder to the textures folder in the target export project - foreach (string texturePath in texturePaths) { - string textureName = Path.GetFileName(texturePath); - string targetPath = texturesDirectory + "\\" + textureName; - File.Copy(texturePath, targetPath); + window.Init(hifiFolder, warnings, OnExportProjectWindowClose); } } @@ -512,11 +495,15 @@ class AvatarExporter : MonoBehaviour { // write out the avatar.fst file to the project directory string exportFstPath = projectDirectory + "avatar.fst"; - WriteFST(exportFstPath, projectName); + if (!WriteFST(exportFstPath, projectName)) { + return; + } // copy any external texture files to the project's texture directory that are considered dependencies of the model texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); - CopyExternalTextures(texturesDirectory); + if (!CopyExternalTextures(texturesDirectory, false)) { + return; + } // remove any double slashes in texture directory path, display success dialog with any // bone warnings previously mentioned, and suggest user to copy external textures over @@ -529,7 +516,7 @@ class AvatarExporter : MonoBehaviour { EditorUtility.DisplayDialog("Success!", successDialog, "Ok"); } - static void WriteFST(string exportFstPath, string projectName) { + static bool WriteFST(string exportFstPath, string projectName) { // write out core fields to top of fst file try { File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + @@ -537,7 +524,7 @@ class AvatarExporter : MonoBehaviour { } catch { EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + ". Please check the location and try again.", "Ok"); - return; + return false; } // write out joint mappings to fst file @@ -596,6 +583,8 @@ class AvatarExporter : MonoBehaviour { // open File Explorer to the project directory once finished System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); + + return true; } static void SetUserBoneInformation() { @@ -625,9 +614,6 @@ class AvatarExporter : MonoBehaviour { } } } - - // generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar - SetFailedBoneRules(); } static void TraverseUserBoneTree(Transform modelBone) { @@ -675,6 +661,29 @@ class AvatarExporter : MonoBehaviour { return result; } + static void AdjustUpperChestMapping() { + if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) { + // if parent of Neck is not Chest then map the parent to UpperChest + string neckUserBone; + if (humanoidToUserBoneMappings.TryGetValue("Neck", out neckUserBone)) { + UserBoneInformation neckParentBoneInfo; + string neckParentUserBone = userBoneInfos[neckUserBone].parentName; + if (userBoneInfos.TryGetValue(neckParentUserBone, out neckParentBoneInfo) && !neckParentBoneInfo.HasHumanMapping()) { + neckParentBoneInfo.humanName = "UpperChest"; + humanoidToUserBoneMappings.Add("UpperChest", neckParentUserBone); + } + } + // if there is still no UpperChest bone but there is a Chest bone then we remap Chest to UpperChest + string chestUserBone; + if (!humanoidToUserBoneMappings.ContainsKey("UpperChest") && + humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { + userBoneInfos[chestUserBone].humanName = "UpperChest"; + humanoidToUserBoneMappings.Remove("Chest"); + humanoidToUserBoneMappings.Add("UpperChest", chestUserBone); + } + } + } + static void SetFailedBoneRules() { failedBoneRules.Clear(); @@ -888,6 +897,43 @@ class AvatarExporter : MonoBehaviour { appendage + " (" + rightCount + ")."); } } + + static string SetTextureDependencies() { + string textureWarnings = ""; + dependencyTextures.Clear(); + + // build the list of all local asset paths for textures that Unity considers dependencies of the model + // for any textures that have duplicate names, return a string of duplicate name warnings + string[] dependencies = AssetDatabase.GetDependencies(assetPath); + foreach (string dependencyPath in dependencies) { + UnityEngine.Object textureObject = AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(Texture2D)); + if (textureObject != null) { + string textureName = Path.GetFileName(dependencyPath); + if (dependencyTextures.ContainsKey(textureName)) { + textureWarnings += "There is more than one texture with the name " + textureName + ".\n\n"; + } else { + dependencyTextures.Add(textureName, dependencyPath); + } + } + } + + return textureWarnings; + } + + static bool CopyExternalTextures(string texturesDirectory, bool overwriteExisting) { + // copy the found dependency textures from the local asset folder to the textures folder in the target export project + foreach (var texture in dependencyTextures) { + string targetPath = texturesDirectory + "\\" + texture.Key; + try { + File.Copy(texture.Value, targetPath, overwriteExisting); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy texture file " + texture.Value + " to " + targetPath + + ". Please check the location and try again.", "Ok"); + return false; + } + } + return true; + } } class ExportProjectWindow : EditorWindow { diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 96a801496686bfb8536dc9fd97d65d8e68868791..3c331ce9e0b03e35850ca6138b53a5689e0e8db0 100644 GIT binary patch literal 13655 zcmV-dHK@uTiwFp#0%lwU0AX@tXmn+5a4vLVasccd>2ll5Ex-K~jD9qBqsXGDGj7^( zZ6$H*BlXd*M=~B-qHH#@sESk^x6PCFM<1!LPyl;#mlS2Y>B-5&;&K;@1+V}Xdo^>9 zpL^ea_V}mSY&Uzo9{i=p_$z+0_V5RQuHb(+$NyHh89ej83s3&X`M;Z{QIzj;{y!Y_admE3VX`J2Hy@T1PAwcfOX|{;3mI!t^8%GN- zyNSHrc{qYUhw&(yrBU7cb+kxhtf$dzP_eTpqKx5al-$n4*?l~__9pQZ${g$skI#l) zvhZ%QZ2o$C`|j?p5fZ>gvbf%!GN9=;&-n_0-ekAajg4iBU@m5W<#2W#&!Qb8X&h(C zLL{Btr&)AszBLX`atV9MbQ+B^z&&lei)PUx9_?&wjHb|$a%A>idq>GE`FV5$lueck z@6Q_^JkZ$Xd<<0gzHl1MmbX`seF6=PAno4!a0;XdGjDVg&aR^rUgDH!QTM`Wnt0G< zIgGIc63@KT;qLy?kTOzDAo^K6@_@zTWH!Br_XRd&_t(4UyQi1K-+VYZJs+N4{(5+N zc5rg+z42T^wCroB_^1KS4l%!a0;|b-Fuc4IUnS zIREYC54)$w2gmO&&xgOkLbc;$7V*XkzMP-zpS<>#X#^myqpu|}fBy-ny?1hSG(6rP z?q6OUA3zP9kY@mW7Tw95$antKu=6xp4Zk(cPBTU$pdF<-6 zx1EimF1R0)?gDRv1wU6%s6Zl4db6?_l3Sa22`Q1{NQLEBlyT7|_vERwJBt)nL2QRFbrvT5YN?L$2Kd><5+Fp#s# zwF=8g?5r{_?2h8`*ohtc5EK+CL63cI6%|zY*@avPo`M89(8J`QD7%fCD%2_{B+<$j za3OVm16I|66{QD71q$(^udSkD5;Loa1GV8jLKZ*m?{AX?d!PJmHzWk=V9t-;Kh7yAO}C4nFM0| z-9qg+8hwV(^w<1y3ht?;aagKSIfX?GA+QU!3JXdxunYNiAzSaTWTkkD%eYZgs;9W5 z#L+J0z|qz|lx(XXyHFXD?7IU?TLam}TxcpCq>3mdR8*iCLHz1KFQ~ej2(S;pqJN1b|D9XTp!8bb`GJD)QgcvRbkX;=aE`TpQkj#pn{48;b$o;C)l>SF$C8Gr9UkuuFHcX-chA*;r@Wxk#iU{?&*m!_1om6)ZYP-3J@BkOh-U3hCY27_*A(4K5*H08dj4HSkn%|X8pWd{f;Xav0h1f`-^ehc1V5YvSD zhyY%xRs$yUYHgSRq}dJzZ68WC5hx5TdO@$*W!QFLDAyYFI)JSW(sqYkgaDlT-Damp zt!)pQ{bs+972%8DY7GYTf-P$X@WraZWy|b(2wcz}v=BBz>$U=-$}4(l2Ti02%hl}% z9lM^q=_{zC-)#*7UWec7FtGFz03$^m!27n@7TZHFtfJZMbXg5P;N6im3_3xx$7|?! zpz4wuR+9_Z67*Uv&X!KQ)#*sKbo%|CsI}b=TJ7SB%IL-T&}sE~B|U&7xzG#RT~SA? zGw62hI+zh%L9Tw!@Ao^LUag?rq$=p8(`;hqL<(EyB$uieyi7~X1$`9 zR=3>}wRXGB?x47$Rr1y834N<5w)Fa)UW+%S(*zFqqA6Y9&-D^H)O%`sBu0fc zT(F(cP`@M8V9@(a4K`ZD8P1OYBNZ{7`HvG4oHcRi%zfC?Mc!AKewDC<|4dXNai-vIz8VH zz&Nts%@#=CKyVYpCt!Wm9z&NB-5*f*;lKT^q;s#;Y^xW45V(Pt62036RqrE`?6u!* zgF0hW$EQoGjgdVF&;!}BM)yzMdF;4=T>~@m{edv?zTfqg=hg0zW}{4?fPm^%qXEpSPtXf~UDQ1HBpJ_r=}5fvg7b`AXj z*ljNR(6RhLG{SF!xC*=94mv$xaOHT0izjWrCuoB8D6gyC3WVjyXR=i(_+eK84e1Gt zad^>FURSdzjeig{fj3(fLM0Ajb$i_|x4poP?m#(ReRw#rpy%MwrgB`xZi(OR!OTAg z7X)CSq|eo3gDc8}4*q1TQV_-=T3zT09YGYo$NUIJp+RRL$k-e7p%<=<*tmKJEEDK) zRGTfZYHC>0C7CAa14Rhjs2JdJi52i!@Eqh+vfK~A%<}w-hY~U0}a- zw!lpl4l9rk#ddYEl~f$*8-OGq_5=zqd9v9(A%MrTU8@Xb~S@QT7Q24QY_j5 z#(%&EL){?oyQoI0gryt}fw4f(7miloYo1mI=*AlY!y=c&{xcjc8SE-Ba0BQ}{nkLb zTJ65(YEcE`X|>5>*q{qsE#_&pdV%z`K)q?6mKYTFK(P8Qp)B#Vm~+KCmH4fEt4@n5 z;=h9;*abG9`B^~RRyUA-7Pt5)bUB&CUn3P6rR6fo#Dh%HY#hRZn(~Dmhr@T5N4p(h=&2S#~nP zlIk<5GFB#+sFxui>N^f$^JQ?3!j-0jA-L)@I5)fvX1k7)>}2+Ckt}Dq4}=B*fh>DT zJR8AE&6x)AJOP;hM59>%w^${|HyC}4lv5o&h<$thc)7%pJZCSO7tre3sI zB#YDoK6vv*^aZN)B3cYdb3q9#?_Au)X~ZGi#nY*m-XwQe^e$Y?AeU1$OJqQwuvLqf zKR-O#`z7cdwh-|2;!tQFJ@22b)-*J6us;&n3cnIP3%@G1+NO#v4!uaD1!g~(fnmC( z**gux`SJ=nideCDgO=?Wtl16_oF?>`ZHs5h=*1H+&ODghyQOM%`qpX%aOdFym?8l4 zS6uM8P?hA0;@V4LU`blABww=@^O-;kT5WC_xma6OE=8MMMx(v5kEkR)`?n%;0FY$ry$@4jC|9yb59p)WaxwFs zGXZNHq{qwY^kgx-ooDx(l0aKK9QLu;D?0w|EW%9>(U@nLz&3=4)AQR33@&J+0>FWe z2soFZ!E#o`AzLgZ3iN+D8oO7?GIP~RAB^hQwvdY~#)1uSu;A@}EmUL^py)nNc z>|H9aK>`fST&CD?oSaBB&JzkVY;K`I)f%u8t>*pZFL@+D(ShH9lJsbP&OEGyRZx+s z78{M`8K^eGSBLXV4uYteq|9efP|l*v>43`|!Y*gbQvSe3 zk4(#D+^1J-1070an1Y3Z_M`8{#;D|F03==;3n{|ZM9PsL2hWns zO99Vx64QoCHYN&5IqMC3t9vfRn2l~Ac!2VE2i0Rh4`9Nt^W9xU@r zF;NCy<)}?U-5vh{w!^9n+^0hf^qLvHfwSpyiuDxJ!@xIzt`Bhp-qT)PO_R}QR$9?2 z!8WKU0ge8V^tT|vkib59W9kfb4LW9KTOT2oFwDwTgC@6JULK>d|$r4K?4dO?%@WY^r8% z<*Bf}jpa~UBI>GFAUT>C%9=6DOuX>`)fGqy)6vlwQba)^LhMZ!q}7;;3KX8|$q66( z^_qz3z0^MLtYCAElTMWtwHEm^BDxpdCSRagvoU1G;m0(d;bh7pxn)W+Of#1UI!K|aKNgQ`q7WzztopXm*Koo}cKJujsRn0DIej^<_xiWjN=G`DB-J;O2t2;CVjxgm`Tk??78DM3Snc&6m&5GvPHb4-&>6`t3HjOUyY*87uzrD z-iwbPUu=~#XPP9Rmvht>f&(mi4UW-ttTYKUo7zxHZw{d-lA0~voX&IHEbBJt zHibS%_q-ZYm736CB({ieg|}IR0l@+?Bfn}?ck!dM#zG+9mY1Ht8UuW;fbO4*vwYjA zmTIPagWi#iZXO&mU_LtvDqzP&IO0l_$uL-0v)8OU9IG`vHs)iVKlTuUj1CHkd3YVh zGc^i(Krb&=XQbZ=YZYOTx*PeM>o3d-VVhNK;w2y5+lmZ(pD+Nq`p^ZEc-j7%xp9ph zW(G4%=$6pAGT}_+qMt&IvuYfGg((1^?F$?m^9qko39%#wpFR_ZnYJTnlo!F1=Z8sH zHA}L9?}G+Ak8fEdi8b+uMa$#tJ_sJ66 z+gaw8kw#ufc-qfu^$IopmhDVEPVoOdGUR1;LXaJfzTv(AD%AYMV6g z`dhw|${sToir7!Svxq%{?|DhGg|?zdHlSFAS|255XeA)~tN5096=xuKRhp^<&-)ih zo{|+!v^ZSA%zH&*$O)V!jJU$>D5??)Q!!x~q{Ar+a<(vlid(RN0*t{O$Rr!hM$*C~ zlzN`RX8o&uQte@i8&6crE9*KFFZNSTaH*%7PxBc6hA3ydHg-R%L2lV09^~SYpq}5rv?cQQcNmw9djW(Jq|L5wUIy zgAeaPtXo9mgstgeV_DD^e@L{IQ7P#Mxspn)VM@L65=Xc~8}T#!O6sCGH4Nn8FX|iB zA5El^J||0161^D*m<##mgr!0(?W-Ysf2u6da@N?i`VQ03Bp7a9!9kveBp zJe5nah{!2Oq=I~qk5S-P3fuv2Y|+|2vgVAC$t!Y4z%!`qaIS6gh1y*$5)W}Ozd%wAL`RdmF ziL5%qOWE@WiL-eA8P>d-IDIRNMlxYg&YTK6rZI>yhy$DFVNwvHPfedSFzRejic<Wx`qa->3BlO$@N+LI4p{*%I8vVZ)CguC)wW4OZt z8OB-3e6%KPY?VyJ@QOazQ+PB=eS3CpQoOE5KQW3`5kdXUrOLRyy5TYpUh#Ede z)j8yp5Gx`#t?EdFJ{1S}OfM|67@>#Fr*&z-ZLa$ev#BnUh|;WE;iLtdW)tZXmZJ&HgU`5B>5sri@d4~J1||o>xC-|8ltg2i`A+V$AQ+B>lSmT zA&gMw%Q;`%MaBsCi;buiUk0R+p45LS0~3$d*#rShnlbhKR+BobdY*bx58nv%ecY(b+HY-shPeVyXP zvm5^;-=>N$BkFbulbt4)IMhDEgH9*>4}5x%KZ1*69!e&*`iclOPL@|VN`VDeHxYFn znB2%J&!2?pJdj>#|umS6h!t=yL7 z;vvu=$$ICyX=8gM=se@yxn^3=z*ZkOtbM9RIeV?;FOpbn)h)Y3+;etH(r1?PIsvTyZx7Af%2yxWy&F^XYva3D}Us z@#SCSsR0kilZ52=if*4&rXS}UvsM^N>`E%=n%IqK4};vN{fZsAKut^a2}Dpjix941 z)4vyp9INbvs#tlb0FyNA2J>Y`DoZWUEmhIglrw|-AibQ|$OVm6>~tMyUmc`LG*tn_ zDR60FwCNP0EfA2!dQxtY#88A!hS*Mlp`?8ur_mpmF$}vtkjT>t#M$^z`>;D3!KfiY zxTeM|m|JuJeQxOv|dm$H=PO$TSVp61dRU(^q>CDHj@QxJ_2&2RXi2$s8o!i=em(g`;;rfSK8MG zEDO_L@6Fdg)jcEk-PXgMM8OXRiu;AEF?o-16F(VP;)l9dX&GYvG215rneB8Z-(m7r zH9wV}ja-nLh~o`ugHK5?6sD#|I6|7m>5ba`A~tEk(CT`Yq%guw8z0!hsm2+^BeKn! z*gDjRzS1o%+iWvUJvYv>BcEzGM3YZDB_ZXar8BzRnNH^(jM5gGy)8bB#m-yxgq1wC zV+zE>mL!@2zeZU~C{WL1Y6O?XBpDwTAsG?1E-8Z=;0u&RSImHp8HBS)Jz6e~GF+9b zWejt$p#oQD9_2WKj-1?U3pDimTpMgcf#Y@>r4Wr7Mb@DUhejzmc7C@i8c6C>VcYf@|<8i;EW0-@*~6e$E%=)J%DW0VW()l+6T$u9VsI0iEt0mgIg=7;uzT-bdgbm>UIm=^C zdPt6=u~S~cJnYfHv0|J-kEWr-Gg>O*;Qcv{6ScsFXQCch&V%}LfcMNfkl(}}wJ`mS5 z-stX-w~6EFn2sQYKR?mfLD~6vuPpTsxhj3V2>^WTvc|AL7mWvv z49q(!td^}aM93d96{o-(+*L5sg^ekUt7WEgg`WDtPoKhTenDjELrz+qe$@!SnDYAQ zo`$^JSc9B~0r#a@Si8!z;BuOR9O8#Kw48AIu<|_f!T?n`ovJnW zG$4bi0ZgBb(Km&^_u4UxL^2&1AF6RuDoA9l`TDpy#FOU>h@gq1+~6NB850XpO@B!q z=y%0`kng14exhy!&~8vb7fNKY4m?Q#4~$BQ49=AV5O`CN3-020t%{YND|kBGJ2^TU z9`6tLFE5S{&VMToB8GDXcjt6p2Zhg;8tg3`g1z!xxVhz(d$RnEwoj^gxGxMS+H7io zFxrq?49*s+jV#Xtr!VlS@%qw+V_~0-3@nl1hd$_6r9X83gb<*3g3+cd0nx)*%HfRd z_C9|F{RFUruWa`xy^cXRVF~)|QI#u~6vw!e_qW<6Sk1WA`ll)O)1(qQ%bP#PAKtpwrUEDcpK8 z6*8RiBfEK!;YIowE>cZ5F(}8y=;odl^Qpz8Qc+SQ-ntlzR+m1eawU~1T458He})*@ zHwUdFPG}a?h)xa8vczPcB$O>xk6)Zv8UnMH^cC*Cx9(rT!uCQ$JxBQwZ#-wEPGN4! zfU4!mIXxD7ll9ZeR5*0wT)7xfZ2MA z1S`_IOZ9HE&f#}!bhF$gT&10D4`(3s0z5Z@QcZN;p9dOr8eM~;cq&}sI|IgNnc648{O zARXaJwqgSkKQdm~6&|&-9($Zi0cgjH)N1R*2t=hOTJsZ7RKD4fI2~|q-eI*()InPg z?wdEOjX69fb=(l){t>xk;c1Q4Y71!`w4~Bx7rk z^32^+7_77D$a-qB4_9>upWNqhU)uQu3S8HUPh)D-{h?0us#!b)@+d6uYRgjZ0KTtl zh5_ayT^84ssZYQ}tugS%tlpy#RhPHsu~SP|XmvDXWch%GRb%#tteis7hD}SX9|ZiA z7c4su^tpjKt^|5+)+dxx5dgFNkAAdH`892cXit$jsj?Te?5RD(9BF5uKDTJ`Pa_4$ z&A%%pp1h68;@_0>Pi@$8S3(yB%+&t#NUh@fty-VP+QMn29B+i~ZPMB}5$D+=FFph9 zg?GZdKlpHV`R?@O0tcxF$Ctm|SkD8(L1ra~+cjH`g+-d>a_f*k6*43@S4FSZy14S8 zgbo2VOZ>#9$>aO5i!Zt?v+W;RlUJV{SuWdK{G2;-iLWTyeN6(!g5BR=#tUvYuOjah zBF_=8&YXTa_kz2oA&aO}i{qJ$d&@pj-zXdF5>$ZB;38P|OS!&Ny32*4@-zzxb&#Z- zL1brmx%Yl}c244QHvF)AN_)K4>`Uh86-|jdpKw`KzCu?16FMznS=ejoYlUv>f=uly zgiK#6K-MR*&lASOH>R3+#Tv1Op5Q?Fz&l|A@g@&O!e>oc13n6cLKr6ou3=Uaugv?& zDj{S1FMbT1*8%*WsP*c3BPe^cRWGB<(^U)_!&Nzv-Kv}k$Wv9G)Ab{%UBmCJ=+6_V z65lAcb%FDbhw*Kky)L>aLiTajBw5;E6JskUj>W9l(e7^!jxLTaFV2Ri?Bc~eFyiNr zn?Uw5Y!s|yzrBhU;-XO!V|C&e@ziX~!+8q)jJ}TGsN<9!j1_*3Z6MZY?#r5jzVgjovtsMn$r$9W)v$tw@i6yP3uV4VYj3rmt7 zBn(!Yq)?c(t=E78vA0f%$mNqu*>lVJo_3RwTGYo@-Lw1C)B=qvi|{?rgE8O8V*pM) zTAu;J%!J!qolk|?Z2|y8ULT;-8uT#b%CFYXlg2!lqWX$y{-x@fLAh!xpWq`9vCF?H z6fCI!5vQ)>qvR})c-8Gj6$}PGE4213k@X`#uj4P#Op22b0OnNLocA`NIIOLm-t(4k zZB$pqc_&R+Z*$4IP|bPoP35h0#e-(pxsB2%Q8OntrWP$mW{nV7kvs{9VYQji*E&=( z^uPZkCfClElS%wFN~M*6WJKoLv<$P1g6hHzf~|I!yPneuXDLH<00x)WnxNQGT4S+dmn%Ur zf(UuT0j8P9zC}T5djlEkG?#bqTl;Qb%x3FLjjosxgvh8uu&MZj8PQ_)w(@`(Rh@Cl z4CiTUf6H;r_Tv$Re)o zFT75GW@S_hB}942fUXzJI|gK4p6tGEON*2akOpIQt-iWV*MHhAO2{G61a*|Nftr2;mqKzad-eopJm=4OUhVccjzNQP z#Apg4=DZT*H76c>Wwa$5l#yfp!@=?X$q$!59PFRJ2Z`u3n>+3t?}rEP-k(F3cGt@C z^NaKIljF;`Cva5J*}?CIkjwApGac^!e0aDb51u!F`SxIVxNl}`7iUz6wJf}br>7^U zCZtwAk2vc7Z142s@K8bXdpbDYefaVC3@+k;zQi#oTB$qfKl{{n?sM2554p4yW{`c; z%E7izp3^oF3o$*Tu=<#lLs1iBE%6mD4@ss|=ibrQX0s0W1ZypvFR#F@psU`l!MPBZ zxzNom`z-O1H@Hw`d5~Vh-uJ-ex(Y|1EsPiBXkh(tQN5)$hzT48E#vybPC99rbyUVHslEnc?{C1 zjAu8=9aqeoG7y1(giO43a(|Q*Nz>s@>y()Rp?;r_SL3d2CmFWDo8#xhx zS|)%kej-to*bIKj18MP`WISBBx%cF#)PFzE}!u$F6P~UPaUL+9-=w8NKK0 zVtTN&Dr9qcO_<{?Uh$x6qf+_GoC?)d1S1&Z_Ype}%+{_pl1%wjqKEX~SeZPIc`d!; z%09z5_mtYbm%tgKP>lXI;Mf{xfjii@{=))~DI`;AH$cd|vk3~2hQ27V0;794nJl^E z73$R%wVGp;&8le}(i$w7>Tj}JjX8bT30F#1Ach*ygUPAU+Y(-PUl z?!y*Qr#@g3~9q=AIGDri-T?jC%a$~cy|a8ou^-CI@MRxt>N=8EjO1{C_2 zn(#&t_hH|E9Y=SY&JuO4zz^{_yD>bdt$NkT6xX95GaI<+92IPpa8dK8EJE02;D{2f zxsPHtQ-r*`<6P?KhTX3WgY9^7FS-ryZP>r~A&t{}&Ark=LNfSc%om1M8247PxfLjk zAILGx67FDTwBnO%3FDwpp*i00G42W###&Ua!Z=82-K|xcH0$*?Sq!PKZWdlyOJ1(d zfdU73(;mebrcvYnn(BvL?k*UJr@0a3+4Q+Ve2U`Hc2JJg3PLi-C4^K2Sb5gr4``F3 z826HD_SqXjpb@sQvG9k8RdWT&?|vW0W0-EChOunyGmnvtV9@p%2q)w?yn-lEzFiHf zhHqDioHxVg_)UUVG?yUcGG9o^Di8W;G$2>m6T!gf_}HLoeju@5*O-c?Bb}ah@u|izE4rc%xZJrqxMsYkZzWY(P7)XgL;5T zw{uiCyjMyIP}UwY@x#mGTlk)EfuS_E=B7xKD6TeFGmdy8f;yo=j&->~vrVpU@+25N zR7UhUu+?)RrI!=iDv{MyZ64(NcA6Yf)|k5UHe8b%YQEyxf9=z3wwu9ZJnFRkey`=X zNBy?nzZx~e-X!SvqSiJ|1vX$UR`~3pPt$KUd%YgiMNj^3Gw24-{Gjc(n~<~ZL;kk! zcb|F9XW!vjqN#b-{4RC<8$K8)cuR|N#UYViYvXzuk6(MML}TM9nI#9ebWigFwq3~k zkJ|(IQik)je)@tyC`*|8s7av8C{r8^;2%J(*7g^y!o`40U#Um{BXS#$4 zvL5V1GJvMFk+{I(HTGW;)SrXf%2qez(|#QS8n6k_`!DXy4|rBFc^jjAXc?b9{8`oi zn?Voze*g-=Z?yx+A9OqI|M&mz@z9c_bely~x7qC6@GI-njZymS|IdH-^N;q9Ww&h@ zma_r>gKpc3M2X);4+Dy|#d_EV3>|iMEV&KV#)j>z&EJorBr8sgdO$F5T}7Y|Te3t` zq)dsVxbQvC%c2EOJjUWUWa(+jqAX5ho~22g%qPbK%rX7)Z=fUp_#5Ounv4LiT>b~g ze+2!{kHUTZFZ3ci|BnIB=zl}3;mAchenHO2men5oGniMAmqzMl7%d0ksqubx(fa_dqjW>EpuX$4 z&gY^5HSM}5XNl14RNR=(sRCA^=8<#Em6FhqdP(jH1JQ2o6cwfi&FytfMRzx?$tXpu zaEf)bQQ*vVJW5dK#A9hILi6R5u7vSv!g0R9GKrgh^i3^;v;fDgb{1e&dV4F6K45TB z$aq#LLZ2#ZNnEK1wWmW-i6$^(j4lD1Y{+{+%=kWI0lbn=iS$tuE$yOf&emPwV=|br zZg@#N`0ub|G|uS~h`1%S^i5OYc`%-JiFO-}C_F1$S+CT24L93T*xK5VNztOV!JLZ> zG?chbK*@KY&jG%e#H>9pq#s4yScla#c@A8|fjnVLo)nF8$Q?L(-H|761u19fg@el`MkT5sFcwh7itG5apMOPaY6C&j6tfIj$eiw$K(xk4%K5BY0T znWBVY#a<(R4oDb(gU+Ol?{QTC@ivv5Zh0d^;n&&yh9Ui`0zzYW4P4paH6FoObHo## z0}`>B?EzkZ6Mi0GqqsXFm#Z7x0 zTS?q{Nxd}Pa(rlsvf0R@DoSzOHec3M7L!VH;TAQv9zsAb%4u`M>`E z9?#$Z`nT=vAlUs90T~8gXX$(q&uhbMV|#l8KkUupXc3QstNUO-`jU)-H_`BOkWPa} zFE~!W1dV#wV;R%g{XDt8Sx{2&9)1RIlTkcL7WdWQU^=V`ko!rtm?u|D1iPG$;(4&R ziG$r)G=x8g$uOQ~aW(i=JkJuWr&g~~v9ma)jL~qI-p-=weKNfc#>oWA9PAB_&jvv{ z4{jEV*{kjCySuwuL;!2){Ca!BfM(k~=Su{7v$&mXY%DVbb1?-h2h;0h8t)iMqhyiJ zMbg=Qwuo=dx7xvpm9UpiCh>3qxM#I@@id+%!<~(d;RHI8MdsjDaFkBdH}MTnHeJqx zKW_x^Kx3D)5l}t&!f7yF-d;iWF*GoSwEN)0DUc#s1jCzXdL3u*l4L}SY7kAbG=MJ4 zVT>e@WEz|fcK444l#yxz(O)FP09ZUqr;~elpJPLIf3!4DvH7Yw))(c=fNdc?(2OrKZ-<_UZ9PeKq9UNc&21?dz^>Dnis?gbogX6*F z-uuDXIh8yceAqqRJ%`E}h&&k3xJ7WZ`|E?Fi=)enGr;-H2?7EX&Y@(Z-PtK>@Zjjf z`EM>i?w%eW9KX9fAN(2%RgTkX%o{8Ca(=RZ@+w$nF@U&^zZSs!-6x>--pSF?;CO$q ze|d3y05xzzo&oe}d?#}v-}zI)&QojD!p1nPRNdszpIrAQ_r0k-Z)(?@+VQ5gy{Rp4 zYSWvV(>iZzecW(T!&={$N^NsfyWZ4}H?{3eZFy6h-qeOSHT0&sZR^%rwUCSo@jpm6DD6(E@wXt-d`N;9-kcSU!I>_zCU<-aQX9-gX8ndB(A~=At;46&|(FC$l z;`?YiqV#570r(eG>1{kpK-)RR8o9+j%+s&Qt*55YQ{*sNEGDrBw-3qm^L;b7gn^t> zu8~_#V&|0cVRw{_MqcdLhh|QZ0`%DDMqWXMpIgX>;3-Iu2R%$~=4E$LQ-vBig(O;b z0UuK5H!!Mtu%h&4UV&V^=xZadn8eH};z4b2kC4Sr_xsy4#oi}>8#yF&y(TxuVR{$O zcjvbX&OC2d;=9LuEM-JIuXj5iCaSIh8$-aBAbTyD$%!j7ZL8^#SLU{%95yY<^^ejE(6!D=W1ECGJI*>ufh zCb)TpZW1r9a-uI%JISo@+n>!u&{a^(ev*ci`Hjk(mX_q`H`6VNuCjEpT%cDnj_2`o z2*1)rL_R3<8o?C~-Y;*XX_|m94~|X1eAeALxUA?qUVQ~0iaEW3=7W?qF@L#?KoqBt z`cv?G-VRO&$9sdz)06YvbLH?97xbE#R7~aBd$bb?e*CfnZ}nQg-)Z+-<1LM*+%|QAf=g5H_j*vakC2+RX1CvJb>f#{1Kz=jsdwtl zCcILu225tvx-bDqz18fuLMT;7px|0`o85YcVcP~*uF>zd0b3WOYYzJe0XX+M^>&w9 z+v?YQ^ym zwp))q^ySpi>oodJUPst%GqCj11V)NFfcGu4Ev|*0TSdLz?ywp{z`HGL=(n5oF0Y~2 zhN=r{SWPZqOS9W(aJIBtjdokIrQPdwMXjw?v(d`0sEA&S5A8;eSJDMYk_+8tt0U@Y zwELZoTL&9NSCFgM4ST&dr&puds#6v8(yrIBeJoeA*=y!iQHow6#C_YGUcE-QN6mUk zFO5#CA!_Y(>YaXmMXThi-R}2=47b8MR4Dk+>~tlE+Q5HPhFupXuak6|^^jN4?{->k z*-64Kah=Iv*zPp)Dkwp$dJlR^oAVt|Z_7T9Vk{fgZguP3ylPg7Rs-;F@Rrn}Mk!le zP$lxE-|B@fKU~Mkt)bHamJ?J6C-!!*rw(`v>Fw9Mpdxc?SWT=B0PZxHf(-k8 z==H<~tQ0f>Q4w=B;k9q^#C6?r+tLB~;ce;m+T8|kO1lmm2t`vmVQBRd>FPc8cqF_+ z7cK@+`!Ka8)L^p%T{&d9_nVDoTWG2}3{GLEZqh_~mi`XV?RQ&s-orwW#g5=(qucKY z$prQ`ZT)IGq<-Kqly{p5od0f!(XH8S0W;N0*Ba_fC)RIJfRHzP^_JjPy&1M!?Ov0c z5q%JK_8YWbvkT*tE0fCO_14hrwSZ%M>}j+?N`zdryWLJ#k_Py><@K2J@NOWPTTJV8 zL!3zKvETIuNMB!Y6U3*<`l{XKXvDE;{t9CY>4mm#SkBcolsd`tu|>k$^;4ss9rXT z;8fE>u1RP>8^%$3=|V-kewZ`Iu39GM;Hd&I?u%gyOj0SD^?DB!Jg=e$0tI$Ni3o*T zL$43xHkWq|Mc3&J|@s1An|#DhT5dtqydBwjfH_Wp)IkP`}+5WbF2P&Gfg%KMlnn6r#0vN-K4E}xY48h`#u!Cx(Ojyd(kPfa85r$SX)GVzw(2X|)+#;XE{xb|M8SKg#;QG*+ zdX2s`wOT#R)S?Q=(rVF&;eyUFwV0*V=r*OL1?o++w1iXG1;Oh1gtEZWV#XEgRN}X? zt=bK$i2rWp!Oj`;nVkj1ZFHK_&H~e+-B)&2qoGE8=oJkgCkresGPEG{(GZ4K7b;Me zR-@OEq;cG06#>jaJp|WB+F8xMW@mM}oRzH*?P(9(0y~SDRxQ#b{CA_NEGwcF|Ji8e zfG(goAcu|!LZMeshfU~*nvvC!rfs`nk6S80>B=|o62y&kZb(|P-Q$5cSWM6bSWOj) z>!HzWK_)6XQbWAX|>d$=6s#MrpZBIu0OHJRL=_pr&|X$Kl}J z<Yap9w5u4S-P5CWFj8Ger3n$WuyoQ; zE0x6R{qFJpI%rvTxBbB!gxt?+^iZEH`_%nn0(HN8p&Evq`^AFle9wcb3?4V#farYn z!&A0_??Wy`YcN*KroS;PMho7+48rLW+M&328pE=@+ZZ@8qFDsKeow(Y13V}3DG0er zJ5KkPGgzGm7#y)Ks(;(f?g8Rr4&NuxS61WzIbD$hMc2uK;d@MsM=Fi9o;Ptn&W5NX zri+s?mQDzG9AK7&8Y_QJO!CSF7H9%#Raa?JWOsAl|zcaK*0XCBh>C3k~6kIF`C1oO}>OQ z%7S=4Pv=7DacaYg2bisc^n_7qZqJMFFP`)Rm8`w%`N1YdK8y%Ica6b_@*fB zel*AlpXxVbMlRM?l}pja%4oD#_7Rn&XaAN*4giu)g7;BI4CP7|=>dIo zv7AqX=S;wA2ifs*GC7$KZfA@8O-Z1w9S-}O)hi5C0GwZLsSPBx_V+^={eGgu_$e%! zP9~#yEC}P~&|6$2DcHVmg&7!j)N3r+kQgHtI^q@`l(KUt&wVoZ(_pD3W~_~6%VNV- zjYX?LZ+O2wPX!##^K9ixQoRz3UW7YOC=x{%KcQw7hT{lUxsOCTN2{=ifG(?0;zgTa zKEO^$)>rd#AV1w4)Fp8Ao+mTvkT+HWx2%451h&-ytjE}_7`rF7*?s{q7Vh{4g38t> zy@D^O?O7(s!?NhQh<`haabH3_;u*%UQy}I|{x3))KnaG$5DKc~W_k?+jGn0!2?uD&AqhWB;wS^Bnh*oaj`f6Kk#ftfR#CLk zo^L@6;`daw1{paQW$V`8_yFY@{z=tq=m#tU zTZ0yFK|^8E$7FE>0y+4)fL%&BK+B3X07ofE>A@}EIdp5hy*9rh>|H9aK>`fST&CD? zoV-ZX&Qpp6Y;K`IRcf$UtP=d?FE$dOVPR-TNqRItrvX;NDk#ZRhmA(_3{-2;G+`s& z`TZ;oo?GPG)xXwAdq1*K3)Ye!vOvNP5I8nUnd?l!Yy|5*71|aD!9b{KNU&41{Gqmk zX*MI{5L~J0D7`C9I`PvPszB6CQra05l(Tr@wUPN~$9+Zi0Z_=6lLfcrnEyv3_;Ma| zC79`@hAGG>_@rDUUS3Vo;b)_ChP1tkHII`#NhH-RkQx%$r(i@}t7_0OGuuW7e(_{E ziXq{iIZrNjneLlH0jz+-w6r*t9S}OgMsUA>WRR>JNF@qXZhnd8mSqEV+P02QK`KXCcSuVm;6;US;ipImETW)KdqEh{~8h2hpC<% zHQ@I`R4caZMs9CoIb=OZ0sMajVxY;Cp)xB@|7njs!RiYO?gguUsqQVvr= z9fhlUEE!|JUJf>#;h*?6{Q}LKjvzBy-IHXB(e`@`{1jtXK8)L#RoJJd>EMt)2B1xq?_yeTrYE z?B)ARuxEI>h>~droo!Q}8S%@b2+O2n3Ls)cWtuKnu#p52;|KVIWu&oF0X5(W2Nl2w zq;?!xEI|Bxeh)?i^~z1LAO}nt1E5z>JwpQ(`al?qFB}&aVU<+?3SOI@@9%UJKGjsX zg~7DjsFyXHMLV^tdK5-r57gJsz-P4eF8*7l$ zkbHu{L|-7MWM3)+^RdL$DE4U$win=dikeuWx3yMTM_jCi*@J-kW!p8Qbhx|)Ap*|n4H@O#7W z$C<7_fSH_COqa;=%u(9?agYGuFN2iqUxO;-;1A(o4Dv6|Qx*jPq$u{X;}h)!=FXs* z-0R#==48L$*Qm;DQ#K7S`kCI)S9U{9=ytbFVWykTfj|p-lFk9bC>}@L24AEEI+$_7 z1Y-gT$dS;hsvxKE*?}{DZ!_*#TO;rZ;Dv7ShiDp4HY;ZgcFllV zsp?dMA$hEXTj!rqlOF6~%%tWs>}m8FMzC*c0>&l-tcqd0`NQ@P z)!>KU|Ne)qV&+WJ^z)JiaKQlz5d*^UWTfmUXg0N>klq|Z-bBMlZ46@0@JBOASqwvU1R}vE^zuYZZkF|X zEiQ#V$M?J%(~vZwZGpy5ZbghZ4+DY)WJZ2fsP6nnXN`k^-If)w(Vhk^>&aS@KW5@j+BK8D$A)(wSJY5|M4;5aq>j6H-Pqnk!z z9$mw@uOc81=;it9jPyHUt)hEUcO!qT{=!BfY_p0BzTlBHSCL`w69a%%AK4e8fNPhT z`#RZKf8;L2cXaGhG*!9ir%>ap8V_JG6oBXY1x{6ZiKh!iSP}!jpNYdv+YvO%i{Q!s z#U!knB{{(NL4%zqw=9awn)q>^ur#iNkgn*|$#geV>H=hqDh<)5u-{+s;rLT;MMcUk z0&5DU&&TRM1E8c|Aj&I+rnhK~1s8r-K# zaPFrIzl_vuA>nC1t5i$W^c%Ku`#8ma_h=w58YcwV;bbUo3$QY>KpEXos8PLEpQ^S_ z%MyOW7aF>Ov0M@R$#)j9NATU2Bv)uliewFnm8kVmVuqsxWPh3560ed4$X%JHD!~i> z1(K&^MH4L!7cdK6k{EIVFH%NaVRqzI35BVc7#XC&DGGA7Fo23%;Qhj2a0fEUVtKI~ z;SoyJrf_ckOfBPag0?EIGT~uA#niF?vF7X8bOogqj2P-EppBZ&F=lTfIkB_2pOMjl zCh!n{(aZrJ%>c6nHv!~GP&kpA!#K+%h`c(VcCOEUiYCTZ&x_^4wY1F{nL`l3Hmchy zi`H56CEkStRAMGPU;+mZV%Ih5>KUXW;#xDTB;s zW-5iEtVq&8SdNqmtKz8!wt37V=2AaBXr#-rZUpXtH@0YPOHCURN%7_liZDKvHhb0{ zc*LNyS|lD~=6r)rsuI+I!>A;g9stxB*@M%s##n+4x&zI6J&(z$lmkz!h85Lv2Ss5m zt&W43qmdeXbYXEuqjG-pLwsj!iN|f0c%elF`5q+B;#uKQ!#q1FjnZCqd2#wy6cb~@ zpiBxC{K^s#V-N>6=gOoYLd%XmYhcs?t`sekP%_#K6W}^>z?1-3qXIUTC83B@N zagmxFDG^;GiQ1?3*kQDPQqV>AkN=RMi_JB{9Vb^9XC?D-G+`EgIu=I0%LQ_i-kzPC z#IEY$x8i`Tn8t^lV?#9A_?$iY5vZKhn67pyxS=9Zn(+x^v26B0q>o{q|6ZKh{JgUB zR7(QPYG1r&o|2s4UvGN9Q1Z@LP0uS;vr@@@+jKzMs>QAOvngtrpsgfV>R3=~EQc??r+fD|En|-JE3~2`UGU^MYi6pU1sW?q z@f7~4Wxk4)ZU6knxc9f?{uR!&CQc;?JF zt>f(ID#;z1V;rdh-lxcNW?Ee~215YlTH}a-YL6uqBB#5imPJKMfM6LB!sThXXBO%|Wa6`GgxoSL z$ESEyVu13~09uWE^ZIOkAS@{Fx`rN)oaZD+iDH=^hn*mjO`%cHfmoj*q$nVDI1r_Jx&s?@UDPdom&+%BB~lqb7H9YB+b zphQN>8Bf}aAuZj z;4~Ef7!lPEnl#&=kQIQfN{F^TXd^Wuk0 zN>t%O^_n_X!fA8KEna|-I*H^KmmJR~_cju6;ULGCf02iHJRDE9&|cB)lgjjCyD{Y# z0zz}H@tQvng`BR5-H3LM$@TUx*$F7rv`n8W2BkBP;8sNadydGl%1)??m1le~NyBb1 zTP}pkQVW7z6}{fx6$bY~dfC>0 zge6@U-ZOAuQyL(FuFe7?!{r=d^AOh^-y?Jrk&ngt2%{HOSegLugF<-b z$rcQo=^VCt0y(lWmI^miN=|QC-N>hXiWT8Y?Q0E|q3N&p=IfuTfsy-e>)}SC;0FW6 z?LyWV+hW|rPX?Cwp&FE0_Gmw5`y?Q?hgIk;=PiTM zCCfA`_rL?2f+nk-Z?2$$`JrkPqf=y1eoZ%X!C8bFS9s?t|EIi04J&$OZP;1N< zPy>8{vS@4>&@qEB2U(w03*D44!e^Nx1+I$Fah3+UYKjVbBY$#+<8~Tn5NR34D`#iO zUpFRZT$uU{%{w3ouZfDPckEK?ZR%i)ovG;*&ExcfY)6X9nRj#%Yxs%)ZBb%^uJ(dXrrb1!9a(L(y0?^9~KV=1RLSeI}? zsf0Q)ZdH__*g!V66w5EUW7Ufnrsbg*_*B!+{qtL31$MRJ{t`i??`t+Fcl&t}YC6f~ z?+0x{*HF`{SZD9K`nBQl!t{CZ+~Z`ZgUH1@){&)joI(b}LN-q5?t41El4~^mfSb4G z!7H1&2Y~r?%el}cEE0|B#2Qz{U2ED+Wv;1Y<5V3UEKJ4>Z>h4__WLOm4~9%=QP^P} zG$`E(T6R_3*EIy7^N2ea>SJ0p}lhpcF2I?GDd4C^BBA8>COO(--*^|OBBT3axm zlFA86I%i9Oi!$yC%Zkeyfug-zNH(#%J^52i*bokovpC+RhXXkpJH;i;b6*V{OU5|# zNFGW&qopDq9_7Q1Tg8r`NCiLCDXbH*3~yCr)aJAh2=g%ryYg(?tK7k-AZn-st%-Kf z(58I>kZUkZy*O)Ob8(V2im_CE(ntbY|EOx00M-H#p4|nY+9jmeV&GoF^V_#;OkTL%Wdl{bJ?qqR~ z7Anqy6Vo|4#1C;MH{tYQ<#FA4?jWt|RQ4$f420S+eL6yY3xDr5)0If31LNigC#8Z! z63ADY%pg89n}fe94y1$s_~HwiA~$t_JazDj{~&#!-hQGEyw@%WK(!#UI0qJFfCu`3 zB7=8k4%}P?Ip?a0SE^Xy;fJS#y_2J(!SViJ|MKGa;QTk@5MMa_aCb)6QINB`)L?I+ zS@AOT;pUfDuATWCZ9G$PZB&Q+oK9+hIKd5a3xi{>+Q{-eFdT?ah1ZwWJWFI;WMImf zZ`#&x0)J>ck`N#p*=W-u1<}K(|6t1Y$)3N2egas*SGI4CUPmCDu=aBLsLGX^MZ0aB zTrcO0!hv;Tw@9O^bvkaH+>c$Z*DwyXJ=T57}e5NHyIgpd9DJn|oRoqn2e! zMM-YGbMF(a(|SzhN-8t7*Gydg8DivKjI)k7;jo~r6lHKMQj>j>P_9%xez8xXTjwm1 z$=$W=TrYx!-Gx1Rj^ZPlc+OIt!d#6ERm+nxWLw~8WhI^2ccq7F+-4}k)9jFj!KcPb zKz^l6TY!Ei3V-68!NjUX7o}28;ChM#E7H14`EIk$;ZM3xgi{o5eqxtO<;)?ffJq5DtJ~wVI#ZrELioF_F2o0w?pPx%X70(K zmik&El(nAy6C9v#D%wL$jTY!6V{4G|%-mBLth0RIc`C9Gmo*2U+~)D$N%;f{T-S)O8}Sgxqp-l68w;%i_`dEi3@{&QviNQUeF7$Gje$2d>OBfkbvLPvotloN z)rOFf8;|n>W@;yAtX3xc zMy!&>_I$1SVc+TbBW#pYge zPysrFi(uI=W%^F&E(VIq(<~&^L6UL?k)6Ti-uuDXIf={J;KS}I?U`7!FPYve4kiA4 z!emwXa#{IL=(L1oZm*@UCAzH-GPNrgGJP!pS)ar{PZa0shkPOQ&n#1`k~aW(RWt#=P6W)Zxq|Q!1>3+-M7{Z@8OTW;XMU3F76HbRIM^ol#~&Ih&I zw{Yqhl2rW9brecpXy|N3Ft_46+a%T#BM(J(%#t=?35|p!FFb%uKk=6{l2~ox=_=MD(gpnUMFAT zsT3z60L-XzIUj67aadbBx#un4+9Of&O=&#N^u9ay(AH#+e)?AQ_RlHb;i(f`aP87_9H; zpD3{E`?kG}lYA69+@_!*5)t)_oZ8E%dPs(p&#|-8PAr4Bu2W~5QjtTp_2(O>(`l`AvFaokKtY5B2kZvn&XkVL`ZNuj&GNm)iNx&6qR z0<4$VF?F)J-U3sJI;jn$18Ou*h_YRUqr{rN<_cFAdyRrpmpR@GSX;vhY;E@*<^77E zl*e8f7ihEGg7nVEgX8^^kCz`0_Rrr#Cu-O0JN_K+2M6!opF@^b$I0^M;{5#N`10)u z?0Y^t`0W64g&jN7;qIHk;fg$XFy-ajgTdjxnX#3hQ6biF@EV++o}8MH8g?FWCg$1R z>B-@tf);jla9ro`b2J%T!~ylrF^GaZd~;*vMX%<^LveJV%8+xQv6F-C4mqb)unuA> zqdM*wvvMdZ?$;1s;ZWdoGVyMWY2f9n^)0Q1v*i`6g{C_|uEC5JhcwfP%=;|yktZ`# zWj4qlW$$|s3%`nnpB;=CB6QTedwsK0N!5Wr)S>G->o%F5!S+v!EPC&$nmtAJQ;c#v zxa=ZXOkx})aC@a#@$B5tpgbg3u5=2>Y^F%KXS>>jhCBvoRK~NL^p2mxX>PU`GV$)> zK7iOYiuyPtlE^P`me1IC2Pz(cc>E zH1!s^gKc>q=XmA`-2~+}K*+qe2?~&gz9_K*qkA|^Aid)i>eU`bu!aq<&BsHp!+Oqc z!w>564lZv#V1Ybq2XP=`aVitpBc zR*kss|EKD>_fNLAd*^>X2N~d%m-(-X-kioGra7485YK^$;y8HRyG3XCzMRLf z5mRK&3S<+%qa^BTBsaUX;&}eYcd*}(1`_Hjr&Sr6d+=!@Pv#M4@5qL#1Mc(N`jKaW z=8EiA19I(4O?YECQjC57RTAHAdP`Kb0w0sn;>NI`w#r-%ock!qYz*A=dIehrT-59- zhY)TV*nzDz_fgDdija4A3>6P==;A%xMVyT9tt*1DfAMowCij|orGtcIu*aA!46QKk zn_{6OD2yM-FkGa}z~sj~aV=q-?ZeZ6<>Lrqt~)DK7;90n3R57}=x$7YP*Cq}Iv-G9 z-NanpkP2D3ItOwL;7!+yF@{Es|7)rrcI&5L;2)ORf+){s2MprV${gD5!jW1*NCvrp zkct2+k0<$vV^W;pRvB%4=C|%6zLektfE89_9O8pg8m&uq?R2u|B)Ae@ln z=n9Tmu-jFmYWQm0*n4HD#cvR_{ENecT-t>+Sp|%s28{;A&Ng>yqUD5iEJg&_Bmi$K z+teXpxvsrEy2-nhbSdMBX8+N?n?tO1k}MjyQW@L=Gx+JfFprh|aT#;{faapO!Es^KV#?2U&R#l5Elc9GGCSOuC(+y5YT2N`M;eArn7uFu8^ADHj+@V^fO7nuKnQUd=e-jR@*Q z207N{22INPy2+DZ^iUZgtk$}tI``5sMsXG&WJnp9UIOYj(KdC+91k{r|~* z>h)H=IUWt$jj-2kgsow(751)%^{6{;_PTLnn+`Fq!DMyx?4eIRtk=8UF4RSj{kPui zG@pgdR@kaT&Q=KdTVdFF7Sx}8hi8d{)wB9{sq5eH!BE3nTD2rj(Fz(H*UMz|Dp(~N z8%OChJ-DSS;pVU|RbI8m&Qr}^1%Jj)3UkoxFw2ovLm&*;)0mXRFox{E`$RzCXhJ$U zA5S_1EFdizVyQnT{2t(geMkn-w0Hy$6n}*pfCTjo7{2VjQ9JEd5ugFPB!d5|gSGlU z#tCt3efIEYRsXLyyV(C5-LTVaHoB0%+3AM=@BiQ9VHeGAvzY8Qo8KFJWqrCf%%1)K z`R{(}jW7&{?Rwk|N25-sHHv!Cs6FgNop=~^JKgPPkAFbN`1`+ltp49BoxHiiPw!q zi7h`U`S;As?yO%?gA@@|@SNhTy*r-yyk76TI_tlSk^VOd=4*ew@O>|d=db-BRO|V2 z6UK}9%D;*OU;U-^pTAs=>%Vi*QT@}K_~IlwGXDpm`Th@7r#Iv=wNmT7kflDgx{&ZLp&r1M}`svec1 zWb#WHN^hBXXkuz7mp5??&|Pn6iNmd|(J3Okazy8_S>cWAcv8@slk)Zz0;~P=NmOS9 zUGFn$)4cJeFG^jM5P00PyFpw;_xCzBG=_sr2mC-2d1X;gcw{bQUx}>1E-0lww}4D) z@fH|syr-!FZpoKId%0mxZFZaFgDG96qdm$><-&ts!-2`X$1M<&BueewX5!&MpJh&# zA7&J;)xGRe6VSTa=V-07F(ud|TXC*2MZXlT6G-tDB5Z`UQ*z?+oI4JoBO^=k+ z;vxM;nl98-Gp?cd?g{mL(cImbA&ri7fk^cI#QqE@%8H(l!FYU&l2CM)SPbz4)p{@u z|3=&(VSL_AF^bN;4?D0R?1KbU`6Jz^Db7a`DT)z;r+m)|w~6_`+>(H_k0*^toX|*( zlL#?I|FO$Nt_qC(Hd94&$6*^T^!Ev9x#aL6ap^k4N*=qt{n{p{sk)%r@)$=>4wuc` zNR#3OCBT`X_SDfP%TpEGIgWc_ec1X%^n}rg2i*V?V50F@=uFylAGa9@uP*d-t12BU zxZ6C`G0CsUAh<@AfXa%FDMwYQh)=i+sU@7AP4nq1`Va$0+17fV+k*( znPztipq(1`=$YoG Date: Thu, 14 Feb 2019 17:51:32 -0800 Subject: [PATCH 32/43] always overwrite, tweak warning --- .../Assets/Editor/AvatarExporter.cs | 14 +++++++------- .../avatarExporter.unitypackage | Bin 13655 -> 13638 bytes 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 624b365c17..1ee1596373 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -453,10 +453,9 @@ class AvatarExporter : MonoBehaviour { return; } - // copy any external texture files to the project's texture directory that are - // considered dependencies of the model, and overwrite any existing ones + // copy any external texture files to the project's texture directory that are considered dependencies of the model string texturesDirectory = Path.GetDirectoryName(exportFstPath) + "\\textures"; - if (!CopyExternalTextures(texturesDirectory, true)) { + if (!CopyExternalTextures(texturesDirectory)) { return; } @@ -501,7 +500,7 @@ class AvatarExporter : MonoBehaviour { // copy any external texture files to the project's texture directory that are considered dependencies of the model texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); - if (!CopyExternalTextures(texturesDirectory, false)) { + if (!CopyExternalTextures(texturesDirectory)) { return; } @@ -910,7 +909,8 @@ class AvatarExporter : MonoBehaviour { if (textureObject != null) { string textureName = Path.GetFileName(dependencyPath); if (dependencyTextures.ContainsKey(textureName)) { - textureWarnings += "There is more than one texture with the name " + textureName + ".\n\n"; + textureWarnings += "There is more than one texture with the name " + textureName + + " referenced in the selected avatar.\n\n"; } else { dependencyTextures.Add(textureName, dependencyPath); } @@ -920,12 +920,12 @@ class AvatarExporter : MonoBehaviour { return textureWarnings; } - static bool CopyExternalTextures(string texturesDirectory, bool overwriteExisting) { + static bool CopyExternalTextures(string texturesDirectory) { // copy the found dependency textures from the local asset folder to the textures folder in the target export project foreach (var texture in dependencyTextures) { string targetPath = texturesDirectory + "\\" + texture.Key; try { - File.Copy(texture.Value, targetPath, overwriteExisting); + File.Copy(texture.Value, targetPath, true); } catch { EditorUtility.DisplayDialog("Error", "Failed to copy texture file " + texture.Value + " to " + targetPath + ". Please check the location and try again.", "Ok"); diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 3c331ce9e0b03e35850ca6138b53a5689e0e8db0..86a47f744e43c7bd7fe88539c3c8de4901f68606 100644 GIT binary patch delta 12560 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 delta 12576 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 CG=KsC From ab5c4a927bf6a4ecd0edcf7a4c85e6d9dc46ee6a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 14 Feb 2019 22:18:42 -0800 Subject: [PATCH 33/43] Fix asset browser being hidden in incorrect way on server switch --- interface/src/Application.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bbe4d70ab6..579cde8861 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6988,17 +6988,19 @@ void Application::nodeActivated(SharedNodePointer node) { #if !defined(DISABLE_QML) auto offscreenUi = getOffscreenUI(); - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - if (assetDialog) { + if (offscreenUi) { auto nodeList = DependencyManager::get(); if (nodeList->getThisNodeCanWriteAssets()) { // call reload on the shown asset browser dialog to get the mappings (if permissions allow) - QMetaObject::invokeMethod(assetDialog, "reload"); + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + if (assetDialog) { + QMetaObject::invokeMethod(assetDialog, "reload"); + } } else { // we switched to an Asset Server that we can't modify, hide the Asset Browser - assetDialog->setVisible(false); + offscreenUi->hide("AssetServer"); } } #endif From 8b2d9c36af1447a6b4f6c1fcf63e7ca75d57f0e4 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Feb 2019 08:39:29 -0800 Subject: [PATCH 34/43] Adding client profile combo. --- tools/nitpick/src/Nitpick.cpp | 8 ++++++-- tools/nitpick/src/Nitpick.h | 2 ++ tools/nitpick/src/Test.cpp | 2 +- tools/nitpick/src/Test.h | 2 +- tools/nitpick/ui/Nitpick.ui | 14 ++++++++++++-- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index d5bc6f6e5a..39800c6bc6 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -40,7 +40,11 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v2.1.2"); + setWindowTitle("Nitpick - v3.0.0"); + + clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; + _ui.clientProfileComboBox->insertItems(0, clientProfiles); + } Nitpick::~Nitpick() { @@ -157,7 +161,7 @@ void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() { } void Nitpick::on_createTestsPushbutton_clicked() { - _test->createTests(); + _test->createTests(_ui.clientProfileComboBox->currentText()); } void Nitpick::on_createMDFilePushbutton_clicked() { diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 36ec7e534b..80fef934d6 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -126,6 +126,8 @@ private: bool _isRunningFromCommandline{ false }; void* _caller; + + QStringList clientProfiles; }; #endif // hifi_Nitpick_h \ No newline at end of file diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index f1e950db88..e8e284bf32 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -391,7 +391,7 @@ void Test::includeTest(QTextStream& textStream, const QString& testPathname) { textStream << "Script.include(testsRootPath + \"" << partialPathWithoutTests + "\");" << endl; } -void Test::createTests() { +void Test::createTests(const QString& clientProfile) { // Rename files sequentially, as ExpectedResult_00000.png, ExpectedResult_00001.png and so on // Any existing expected result images will be deleted QString previousSelection = _snapshotDirectory; diff --git a/tools/nitpick/src/Test.h b/tools/nitpick/src/Test.h index 166c71688d..23011d0c31 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -52,7 +52,7 @@ public: void finishTestsEvaluation(); - void createTests(); + void createTests(const QString& clientProfile); void createTestsOutline(); diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 79bdfd158b..47471522db 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -52,8 +52,8 @@ - 210 - 60 + 70 + 40 220 40 @@ -153,6 +153,16 @@ Create all testAuto scripts + + + + 320 + 40 + 120 + 40 + + + From b77f40d300d8996adb6d318f38a0eeb9b1b44793 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Feb 2019 09:52:57 -0800 Subject: [PATCH 35/43] Revert due to stable build problem on Ubuntu. --- tools/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index b9ae635a4f..6cda67db2d 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -20,7 +20,7 @@ endfunction() if (BUILD_TOOLS) # Allow different tools for stable builds - if (STABLE_BUILD) + if (RELEASE_TYPE STREQUAL "PRODUCTION") set(ALL_TOOLS udt-test vhacd-util From 54839718555c23f11afc322bd9c693844cae97e0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Feb 2019 10:46:08 -0800 Subject: [PATCH 36/43] Fix crash in vive and interleaved stereo plugins --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 2 +- .../src/display-plugins/OpenGLDisplayPlugin.h | 5 +++++ libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index e4deaf8f4b..20fc9a2290 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -720,7 +720,7 @@ void OpenGLDisplayPlugin::present() { } gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); - } else { + } else if (alwaysPresent()) { internalPresent(); } _movingAveragePresent.addSample((float)(usecTimestampNow() - startPresent)); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 5c653f8a0a..49a38ecb4c 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -88,6 +88,11 @@ protected: glm::uvec2 getSurfaceSize() const; glm::uvec2 getSurfacePixels() const; + // Some display plugins require us to always execute some present logic, + // whether we have a frame or not (Oculus Mobile plugin) + // Such plugins must be prepared to do the right thing if the `_currentFrame` + // is not populated + virtual bool alwaysPresent() const { return false; } void updateCompositeFramebuffer(); diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h index 4a0a21e995..a98989655e 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.h @@ -57,6 +57,7 @@ protected: void internalPresent() override; void hmdPresent() override { throw std::runtime_error("Unused"); } bool isHmdMounted() const override; + bool alwaysPresent() const override { return true; } static const char* NAME; mutable gl::Context* _mainContext{ nullptr }; From a786a832d9ab2322ad1b82d213fa406e275d516f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 15 Feb 2019 09:28:03 -0800 Subject: [PATCH 37/43] Fix android release build APK --- android/build_android.sh | 2 +- android/containerized_build.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/android/build_android.sh b/android/build_android.sh index 9c68b8969b..a066332f9a 100755 --- a/android/build_android.sh +++ b/android/build_android.sh @@ -3,7 +3,7 @@ set -xeuo pipefail ./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET} # This is the actual output from gradle, which no longer attempts to muck with the naming of the APK -OUTPUT_APK=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_DIR}/${ANDROID_APP}-${ANDROID_BUILD_DIR}.apk +OUTPUT_APK=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_DIR}/${ANDROID_BUILT_APK_NAME} # This is the APK name requested by Jenkins TARGET_APK=./${ANDROID_APK_NAME} # Make sure this matches up with the new ARTIFACT_EXPRESSION for jenkins builds, which should be "android/*.apk" diff --git a/android/containerized_build.sh b/android/containerized_build.sh index 60d5c9db98..8b2f26cb50 100755 --- a/android/containerized_build.sh +++ b/android/containerized_build.sh @@ -16,6 +16,7 @@ docker run \ -e RELEASE_NUMBER \ -e RELEASE_TYPE \ -e ANDROID_APP \ + -e ANDROID_BUILT_APK_NAME \ -e ANDROID_APK_NAME \ -e ANDROID_BUILD_TARGET \ -e ANDROID_BUILD_DIR \ From 556a55ff160bc936ef2db2fc58edc9f9bf2b7bbc Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Feb 2019 20:55:27 +0100 Subject: [PATCH 38/43] Better scale and texture checks --- .../avatarPackager/AvatarDoctorDiagnose.qml | 64 +++++---- .../avatarPackager/CreateAvatarProject.qml | 2 +- interface/src/avatar/AvatarDoctor.cpp | 134 +++++++++++++++--- interface/src/avatar/AvatarDoctor.h | 5 + interface/src/avatar/AvatarProject.cpp | 4 +- 5 files changed, 151 insertions(+), 58 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml index d329b903bd..302930dee0 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml @@ -4,39 +4,59 @@ import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 Item { - id: diagnosingScreen + id: root visible: false property var avatarDoctor: null property var errors: [] + property int minimumDiagnoseTimeMS: 1000 + signal doneDiagnosing onVisibleChanged: { - if (!diagnosingScreen.visible) { - //if (debugDelay.running) { - // debugDelay.stop(); - //} + if (root.avatarDoctor !== null) { + root.avatarDoctor.complete.disconnect(_private.avatarDoctorComplete); + root.avatarDoctor = null; + } + if (doneTimer.running) { + doneTimer.stop(); + } + + if (!root.visible) { return; } - //debugDelay.start(); - avatarDoctor = AvatarPackagerCore.currentAvatarProject.diagnose(); - avatarDoctor.complete.connect(function(errors) { + + root.avatarDoctor = AvatarPackagerCore.currentAvatarProject.diagnose(); + root.avatarDoctor.complete.connect(this, _private.avatarDoctorComplete); + _private.startTime = Date.now(); + root.avatarDoctor.startDiagnosing(); + } + + QtObject { + id: _private + property real startTime: 0 + + function avatarDoctorComplete(errors) { + if (!root.visible) { + return; + } + console.warn("avatarDoctor.complete " + JSON.stringify(errors)); - diagnosingScreen.errors = errors; + root.errors = errors; AvatarPackagerCore.currentAvatarProject.hasErrors = errors.length > 0; AvatarPackagerCore.addCurrentProjectToRecentProjects(); - // FIXME: can't seem to change state here so do it with a timer instead + let timeSpendDiagnosingMS = Date.now() - _private.startTime; + let timeLeftMS = root.minimumDiagnoseTimeMS - timeSpendDiagnosingMS; + doneTimer.interval = timeLeftMS < 0 ? 0 : timeLeftMS; doneTimer.start(); - }); - avatarDoctor.startDiagnosing(); + } } Timer { id: doneTimer - interval: 1 repeat: false running: false onTriggered: { @@ -44,24 +64,6 @@ Item { } } -/* - Timer { - id: debugDelay - interval: 5000 - repeat: false - running: false - onTriggered: { - if (Math.random() > 0.5) { - // ERROR - avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport; - } else { - // SUCCESS - avatarPackager.state = AvatarPackagerState.project; - } - } - } -*/ - property var footer: Item { anchors.fill: parent anchors.rightMargin: 17 diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index c299417c27..a0149b118f 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -32,7 +32,7 @@ Item { return; } avatarProject.reset(); - avatarPackager.state = AvatarPackagerState.project; + avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; } } } diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index d2397ed21f..c8f5d52336 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -11,6 +11,8 @@ #include "AvatarDoctor.h" #include +#include +#include AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : _avatarFSTFileUrl(std::move(avatarFSTFileUrl)) { @@ -18,63 +20,149 @@ AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : void AvatarDoctor::startDiagnosing() { _errors.clear(); + + _externalTextureCount = 0; + _checkedTextureCount = 0; + _missingTextureCount = 0; + _unsupportedTextureCount = 0; + const auto resource = DependencyManager::get()->getGeometryResource(_avatarFSTFileUrl); - const auto resourceLoaded = [this, resource](bool success) { + resource->refresh(); + const QUrl DEFAULT_URL = QUrl("https://docs.highfidelity.com/create/avatars/create-avatars.html#create-your-own-avatar"); + const auto resourceLoaded = [this, resource, DEFAULT_URL](bool success) { // MODEL if (!success) { - _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Model file cannot be opened", DEFAULT_URL }); emit complete(getErrors()); return; } + const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); if (!avatarModel.originalURL.endsWith(".fbx")) { - _errors.push_back({ "Unsupported avatar model format", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Unsupported avatar model format", DEFAULT_URL }); emit complete(getErrors()); return; } // RIG if (avatarModel.joints.isEmpty()) { - _errors.push_back({ "Avatar has no rig", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Avatar has no rig", DEFAULT_URL }); } else { if (avatarModel.joints.length() > 256) { - _errors.push_back({ "Avatar has over 256 bones", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Avatar has over 256 bones", DEFAULT_URL }); } // Avatar does not have Hips bone mapped if (!avatarModel.getJointNames().contains("Hips")) { - _errors.push_back({ "Hips are not mapped", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Hips are not mapped", DEFAULT_URL }); } if (!avatarModel.getJointNames().contains("Spine")) { - _errors.push_back({ "Spine is not mapped", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Spine is not mapped", DEFAULT_URL }); } if (!avatarModel.getJointNames().contains("Head")) { - _errors.push_back({ "Head is not mapped", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Head is not mapped", DEFAULT_URL }); } } // SCALE - const float DEFAULT_HEIGHT = 1.75f; - const float RECOMMENDED_MIN_HEIGHT = DEFAULT_HEIGHT * 0.25; - const float RECOMMENDED_MAX_HEIGHT = DEFAULT_HEIGHT * 1.5; + const float RECOMMENDED_MIN_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; + + const float avatarHeight = avatarModel.bindExtents.largestDimension(); - float avatarHeight = avatarModel.getMeshExtents().largestDimension(); - - qWarning() << "avatarHeight" << avatarHeight; + qDebug() << "avatarHeight" << avatarHeight; + qDebug() << "defined Scale =" << model->getMapping()["scale"].toFloat(); if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly smaller then expected.", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Avatar is possibly smaller then expected.", DEFAULT_URL }); + } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { + _errors.push_back({ "Avatar is possibly larger then expected.", DEFAULT_URL }); } - else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { - _errors.push_back({ "Avatar is possibly larger then expected.", QUrl("http://www.highfidelity.com/docs") }); - } - - // BLENDSHAPES // TEXTURES - //avatarModel.materials. + QStringList externalTextures{}; + QSet textureNames{}; + auto addTextureToList = [&externalTextures](hfm::Texture texture) mutable { + if (!texture.filename.isEmpty() && texture.content.isEmpty() && !externalTextures.contains(texture.name)) { + externalTextures << texture.name; + } + }; + + foreach(const HFMMaterial material, avatarModel.materials) { + addTextureToList(material.normalTexture); + addTextureToList(material.albedoTexture); + addTextureToList(material.opacityTexture); + addTextureToList(material.glossTexture); + addTextureToList(material.roughnessTexture); + addTextureToList(material.specularTexture); + addTextureToList(material.metallicTexture); + addTextureToList(material.emissiveTexture); + addTextureToList(material.occlusionTexture); + addTextureToList(material.scatteringTexture); + addTextureToList(material.lightmapTexture); + } + if (!externalTextures.empty()) { + // Check External Textures: + auto modelTexturesURLs = model->getTextures(); + _externalTextureCount = externalTextures.length(); + foreach(const QString textureKey, externalTextures) { + if (!modelTexturesURLs.contains(textureKey)) { + _missingTextureCount++; + _checkedTextureCount++; + continue; + } - emit complete(getErrors()); + const QUrl textureURL = modelTexturesURLs[textureKey].toUrl(); + + auto textureResource = DependencyManager::get()->getTexture(textureURL); + auto checkTextureLoadingComplete = [this, DEFAULT_URL] () mutable { + qDebug() << "checkTextureLoadingComplete" << _checkedTextureCount << "/" << _externalTextureCount; + + if (_checkedTextureCount == _externalTextureCount) { + if (_missingTextureCount == 1) { + _errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_URL }); + } + if (_unsupportedTextureCount > 0) { + _errors.push_back({ tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), DEFAULT_URL }); + } + emit complete(getErrors()); + } + }; + + auto textureLoaded = [this, textureResource, checkTextureLoadingComplete] (bool success) mutable { + if (!success) { + auto normalizedURL = DependencyManager::get()->normalizeURL(textureResource->getURL()); + if (normalizedURL.isLocalFile()) { + QFile textureFile(normalizedURL.toLocalFile()); + if (textureFile.exists()) { + _unsupportedTextureCount++; + } else { + _missingTextureCount++; + } + } else { + _missingTextureCount++; + } + } + _checkedTextureCount++; + checkTextureLoadingComplete(); + }; + + if (textureResource) { + textureResource->refresh(); + if (textureResource->isLoaded()) { + textureLoaded(!textureResource->isFailed()); + } else { + connect(textureResource.data(), &NetworkTexture::finished, this, textureLoaded); + } + } else { + _missingTextureCount++; + _checkedTextureCount++; + checkTextureLoadingComplete(); + } + } + } else { + emit complete(getErrors()); + } }; if (resource) { @@ -84,7 +172,7 @@ void AvatarDoctor::startDiagnosing() { connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); } } else { - _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Model file cannot be opened", DEFAULT_URL }); emit complete(getErrors()); } } diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index 65a184af71..f11bc7377c 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -45,6 +45,11 @@ signals: private: QUrl _avatarFSTFileUrl; QVector _errors; + + int _externalTextureCount = 0; + int _checkedTextureCount = 0; + int _missingTextureCount = 0; + int _unsupportedTextureCount = 0; }; #endif // hifi_AvatarDoctor_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 74edabd1f5..b020cdb627 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -244,9 +244,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { } AvatarDoctor* AvatarProject::diagnose() { - auto avatarDoctor = new AvatarDoctor(QUrl(getFSTPath())); - - return avatarDoctor; + return new AvatarDoctor(QUrl(getFSTPath())); } void AvatarProject::openInInventory() const { From 2c4f485079a90f55c5f175f14fd0899a2c6baa5f Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 15 Feb 2019 12:31:01 -0800 Subject: [PATCH 39/43] QmlMarketplace invert arrow direction on sort. --- .../resources/qml/hifi/commerce/marketplace/SortButton.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml index e876842d89..2673043b6b 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml @@ -28,8 +28,8 @@ Item { id: root; - property string ascGlyph: "\u2191" - property string descGlyph: "\u2193" + property string ascGlyph: "\u2193" + property string descGlyph: "\u2191" property string text: "" property bool ascending: false property bool checked: false From 1154414a070228c2580eb2e868000ff20f97cc96 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 15 Feb 2019 12:36:35 -0800 Subject: [PATCH 40/43] remove unneeded marketplaceInject.js --- scripts/system/html/js/marketplacesInject.js | 744 ------------------- 1 file changed, 744 deletions(-) delete mode 100644 scripts/system/html/js/marketplacesInject.js diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js deleted file mode 100644 index 8d408169ba..0000000000 --- a/scripts/system/html/js/marketplacesInject.js +++ /dev/null @@ -1,744 +0,0 @@ -/* global $, window, MutationObserver */ - -// -// marketplacesInject.js -// -// Created by David Rowe on 12 Nov 2016. -// Copyright 2016 High Fidelity, Inc. -// -// Injected into marketplace Web pages. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function () { - // Event bridge messages. - var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; - var CLARA_IO_STATUS = "CLARA.IO STATUS"; - var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; - var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; - var GOTO_DIRECTORY = "GOTO_DIRECTORY"; - var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; - var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; - var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; - var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; - - var canWriteAssets = false; - 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; - - function injectCommonCode(isDirectoryPage) { - // Supporting styles from marketplaces.css. - // Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain. - $("head").append( - '' - ); - - // Supporting styles from edit-style.css. - // Font family, size, and position adjusted because Raleway-Bold cannot be used cross-domain. - $("head").append( - '' - ); - - // Footer. - var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace?"); - $("body").append( - '
' + - (!isInitialHiFiPage ? '' : '') + - (isInitialHiFiPage ? '🛈 Get items from Clara.io!' : '') + - (!isDirectoryPage ? '' : '') + - (isDirectoryPage ? '🛈 Select a marketplace to explore.' : '') + - '
' - ); - - // Footer actions. - $("#back-button").on("click", function () { - if (document.referrer !== "") { - window.history.back(); - } else { - var params = { type: GOTO_MARKETPLACE }; - var itemIdMatch = location.search.match(/itemId=([^&]*)/); - if (itemIdMatch && itemIdMatch.length === 2) { - params.itemId = itemIdMatch[1]; - } - EventBridge.emitWebEvent(JSON.stringify(params)); - } - }); - $("#all-markets").on("click", function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: GOTO_DIRECTORY - })); - }); - } - - function injectDirectoryCode() { - - // Remove e-mail hyperlink. - var letUsKnow = $("#letUsKnow"); - letUsKnow.replaceWith(letUsKnow.html()); - - // Add button links. - - $('#exploreClaraMarketplace').on('click', function () { - window.location = "https://clara.io/library?gameCheck=true&public=true"; - }); - $('#exploreHifiMarketplace').on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: GOTO_MARKETPLACE - })); - }); - } - - 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. - - // Clara library page. - if (location.href.indexOf("clara.io/library") !== -1) { - // Make entries navigate to "Image" view instead of default "Real Time" view. - var elements = $("a.thumbnail"); - for (var i = 0, length = elements.length; i < length; i++) { - var value = elements[i].getAttribute("href"); - if (value.slice(-6) !== "/image") { - elements[i].setAttribute("href", value + "/image"); - } - } - } - - // Clara item page. - if (location.href.indexOf("clara.io/view/") !== -1) { - // Make site navigation links retain gameCheck etc. parameters. - var element = $("a[href^=\'/library\']")[0]; - var parameters = "?gameCheck=true&public=true"; - var href = element.getAttribute("href"); - if (href.slice(-parameters.length) !== parameters) { - element.setAttribute("href", href + parameters); - } - - // Remove unwanted buttons and replace download options with a single "Download to High Fidelity" button. - var buttons = $("a.embed-button").parent("div"); - var downloadFBX; - if (buttons.find("div.btn-group").length > 0) { - buttons.children(".btn-primary, .btn-group , .embed-button").each(function () { this.remove(); }); - if ($("#hifi-download-container").length === 0) { // Button hasn't been moved already. - downloadFBX = $(' Download to High Fidelity'); - buttons.prepend(downloadFBX); - downloadFBX[0].addEventListener("click", startAutoDownload); - } - } - - // Move the "Download to High Fidelity" button to be more visible on tablet. - if ($("#hifi-download-container").length === 0 && window.innerWidth < 700) { - var downloadContainer = $('
'); - $(".top-title .col-sm-4").append(downloadContainer); - downloadContainer.append(downloadFBX); - } - } - } - - // Automatic download to High Fidelity. - function startAutoDownload() { - // One file request at a time. - if (isPreparing) { - console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); - return; - } - - // User must be able to write to Asset Server. - if (!canWriteAssets) { - console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); - EventBridge.emitWebEvent(JSON.stringify({ - type: WARN_USER_NO_PERMISSIONS - })); - return; - } - - // User must be logged in. - var loginButton = $("#topnav a[href='/signup']"); - if (loginButton.length > 0) { - loginButton[0].click(); - return; - } - - // Obtain zip file to download for requested asset. - // Reference: https://clara.io/learn/sdk/api/export - - //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; - // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to - // be successful in generating zip files. - var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; - - var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; - var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); - - xmlHttpRequest = new XMLHttpRequest(); - var responseTextIndex = 0; - var zipFileURL = ""; - - xmlHttpRequest.onreadystatechange = function () { - // Messages are appended to responseText; process the new ones. - var message = this.responseText.slice(responseTextIndex); - var statusMessage = ""; - - if (isPreparing) { // Ignore messages in flight after finished/cancelled. - var lines = message.split(/[\n\r]+/); - - for (var i = 0, length = lines.length; i < length; i++) { - if (lines[i].slice(0, 5) === "data:") { - // Parse line. - var data; - try { - data = JSON.parse(lines[i].slice(5)); - } - catch (e) { - data = {}; - } - - // Extract zip file URL. - if (data.hasOwnProperty("files") && data.files.length > 0) { - zipFileURL = data.files[0].url; - } - } - } - - if (statusMessage !== "") { - // Update the UI with the most recent status message. - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: statusMessage - })); - } - } - - responseTextIndex = this.responseText.length; - }; - - // Note: onprogress doesn't have computable total length so can't use it to determine % complete. - - xmlHttpRequest.onload = function () { - var statusMessage = ""; - - if (!isPreparing) { - return; - } - - isPreparing = false; - - var HTTP_OK = 200; - if (this.status !== HTTP_OK) { - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: statusMessage - })); - } else if (zipFileURL.slice(-4) !== ".zip") { - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: (statusMessage + ": " + zipFileURL) - })); - } else { - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_DOWNLOAD - })); - } - - xmlHttpRequest = null; - } - - isPreparing = true; - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_STATUS, - status: "Initiating download" - })); - - xmlHttpRequest.open("POST", url, true); - xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); - xmlHttpRequest.send(); - } - - function injectClaraCode() { - - // Make space for marketplaces footer in Clara pages. - $("head").append( - '' - ); - - // Condense space. - $("head").append( - '' - ); - - // Move "Download to High Fidelity" button. - $("head").append( - '' - ); - - // Update code injected per page displayed. - var updateClaraCodeInterval = undefined; - updateClaraCode(); - updateClaraCodeInterval = setInterval(function () { - updateClaraCode(); - }, 1000); - - window.addEventListener("unload", function () { - clearInterval(updateClaraCodeInterval); - updateClaraCodeInterval = undefined; - }); - - EventBridge.emitWebEvent(JSON.stringify({ - type: QUERY_CAN_WRITE_ASSETS - })); - } - - function cancelClaraDownload() { - isPreparing = false; - - if (xmlHttpRequest) { - xmlHttpRequest.abort(); - xmlHttpRequest = null; - console.log("Clara.io FBX: File download cancelled"); - EventBridge.emitWebEvent(JSON.stringify({ - type: CLARA_IO_CANCELLED_DOWNLOAD - })); - } - } - - function injectCode() { - var DIRECTORY = 0; - var HIFI = 1; - var CLARA = 2; - var HIFI_ITEM_PAGE = 3; - var pageType = DIRECTORY; - - if (location.href.indexOf(marketplaceBaseURL + "/") !== -1) { pageType = HIFI; } - if (location.href.indexOf("clara.io/") !== -1) { pageType = CLARA; } - if (location.href.indexOf(marketplaceBaseURL + "/marketplace/items/") !== -1) { pageType = HIFI_ITEM_PAGE; } - - injectCommonCode(pageType === DIRECTORY); - switch (pageType) { - case DIRECTORY: - injectDirectoryCode(); - break; - case HIFI: - injectHiFiCode(); - break; - case CLARA: - injectClaraCode(); - break; - case HIFI_ITEM_PAGE: - injectHiFiItemPageCode(); - break; - - } - } - - function onLoad() { - EventBridge.scriptEventReceived.connect(function (message) { - message = JSON.parse(message); - if (message.type === CAN_WRITE_ASSETS) { - canWriteAssets = message.canWriteAssets; - } else if (message.type === CLARA_IO_CANCEL_DOWNLOAD) { - 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.', ''); - } - messagesWaiting = message.data.messagesWaiting; - injectCode(); - } - } - }); - - // Request commerce setting - // Code is injected into the webpage after the setting comes back. - EventBridge.emitWebEvent(JSON.stringify({ - type: "REQUEST_SETTING" - })); - } - - // Load / unload. - window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). - window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed -}()); From 348ac3167ffb8ba20a25e86b5a6a69407598d36d Mon Sep 17 00:00:00 2001 From: David Back Date: Fri, 15 Feb 2019 12:58:30 -0800 Subject: [PATCH 41/43] move failed bone rules call, add get texture directory --- .../Assets/Editor/AvatarExporter.cs | 19 +++++++++++------- .../avatarExporter.unitypackage | Bin 13638 -> 13667 bytes 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 1ee1596373..7b90145223 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -307,9 +307,6 @@ class AvatarExporter : MonoBehaviour { SetUserBoneInformation(); string textureWarnings = SetTextureDependencies(); - // generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar - SetFailedBoneRules(); - // check if we should be substituting a bone for a missing UpperChest mapping AdjustUpperChestMapping(); @@ -454,7 +451,7 @@ class AvatarExporter : MonoBehaviour { } // copy any external texture files to the project's texture directory that are considered dependencies of the model - string texturesDirectory = Path.GetDirectoryName(exportFstPath) + "\\textures"; + string texturesDirectory = GetTextureDirectory(exportFstPath); if (!CopyExternalTextures(texturesDirectory)) { return; } @@ -487,7 +484,7 @@ class AvatarExporter : MonoBehaviour { File.Copy(assetPath, exportModelPath); // create empty Textures and Scripts folders in the project directory - string texturesDirectory = projectDirectory + "\\textures"; + string texturesDirectory = GetTextureDirectory(projectDirectory); string scriptsDirectory = projectDirectory + "\\scripts"; Directory.CreateDirectory(texturesDirectory); Directory.CreateDirectory(scriptsDirectory); @@ -499,7 +496,6 @@ class AvatarExporter : MonoBehaviour { } // copy any external texture files to the project's texture directory that are considered dependencies of the model - texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); if (!CopyExternalTextures(texturesDirectory)) { return; } @@ -613,6 +609,9 @@ class AvatarExporter : MonoBehaviour { } } } + + // generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar + SetFailedBoneRules(); } static void TraverseUserBoneTree(Transform modelBone) { @@ -897,6 +896,12 @@ class AvatarExporter : MonoBehaviour { } } + static string GetTextureDirectory(string basePath) { + string textureDirectory = Path.GetDirectoryName(basePath) + "\\textures"; + textureDirectory = textureDirectory.Replace("\\\\", "\\"); + return textureDirectory; + } + static string SetTextureDependencies() { string textureWarnings = ""; dependencyTextures.Clear(); @@ -910,7 +915,7 @@ class AvatarExporter : MonoBehaviour { string textureName = Path.GetFileName(dependencyPath); if (dependencyTextures.ContainsKey(textureName)) { textureWarnings += "There is more than one texture with the name " + textureName + - " referenced in the selected avatar.\n\n"; + " referenced in the selected avatar.\n\n"; } else { dependencyTextures.Add(textureName, dependencyPath); } diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 86a47f744e43c7bd7fe88539c3c8de4901f68606..95c000e7c61b7623e0da0a1f9974e0744375014e 100644 GIT binary patch literal 13667 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 B&65BC literal 13638 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 Date: Fri, 15 Feb 2019 11:14:49 -0800 Subject: [PATCH 42/43] Fix GLTF materials --- .../src/material-networking/TextureCache.cpp | 11 ++++++----- .../src/material-networking/TextureCache.h | 2 +- .../src/model-networking/ModelCache.cpp | 4 ++-- libraries/networking/src/ResourceCache.cpp | 4 ++-- libraries/networking/src/ResourceCache.h | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index ee3c88f02c..9a9720c87d 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -368,16 +368,17 @@ static bool isLocalUrl(const QUrl& url) { return (scheme == HIFI_URL_SCHEME_FILE || scheme == URL_SCHEME_QRC || scheme == RESOURCE_SCHEME); } -void NetworkTexture::setExtra(void* extra) { +void NetworkTexture::setExtra(void* extra, bool isNewExtra) { const TextureExtra* textureExtra = static_cast(extra); _type = textureExtra ? textureExtra->type : image::TextureUsage::DEFAULT_TEXTURE; _maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; _sourceChannel = textureExtra ? textureExtra->sourceChannel : image::ColorChannel::NONE; - if (_textureSource) { - _textureSource->setUrl(_url); - _textureSource->setType((int)_type); - } else { + if (isNewExtra && !_loaded) { + _startedLoading = false; + } + + if (!_textureSource || isNewExtra) { _textureSource = std::make_shared(_url, (int)_type); } _lowestRequestedMipLevel = 0; diff --git a/libraries/material-networking/src/material-networking/TextureCache.h b/libraries/material-networking/src/material-networking/TextureCache.h index acca916acc..a8b152c40e 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.h +++ b/libraries/material-networking/src/material-networking/TextureCache.h @@ -64,7 +64,7 @@ public: Q_INVOKABLE void setOriginalDescriptor(ktx::KTXDescriptor* descriptor) { _originalKtxDescriptor.reset(descriptor); } - void setExtra(void* extra) override; + void setExtra(void* extra, bool isNewExtra) override; signals: void networkTextureCreated(const QWeakPointer& self); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 581196b2cc..b2645d20c8 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -309,7 +309,7 @@ public: virtual void downloadFinished(const QByteArray& data) override; - void setExtra(void* extra) override; + void setExtra(void* extra, bool isNewExtra) override; protected: Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping); @@ -320,7 +320,7 @@ private: bool _combineParts; }; -void GeometryDefinitionResource::setExtra(void* extra) { +void GeometryDefinitionResource::setExtra(void* extra, bool isNewExtra) { const GeometryExtra* geometryExtra = static_cast(extra); _mapping = geometryExtra ? geometryExtra->mapping : QVariantHash(); _textureBaseUrl = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl(); diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 7345081380..8ad1b41020 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -355,7 +355,7 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& } else if (resourcesWithExtraHash.size() > 0.0f) { // We haven't seen this extra info before, but we've already downloaded the resource. We need a new copy of this object (with any old hash). resource = createResourceCopy(resourcesWithExtraHash.begin().value().lock()); - resource->setExtra(extra); + resource->setExtra(extra, true); resource->setExtraHash(extraHash); resource->setSelf(resource); resource->setCache(this); @@ -375,7 +375,7 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& if (!resource) { resource = createResource(url); - resource->setExtra(extra); + resource->setExtra(extra, false); resource->setExtraHash(extraHash); resource->setSelf(resource); resource->setCache(this); diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 2096213273..62800a6ac2 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -417,7 +417,7 @@ public: unsigned int getDownloadAttempts() { return _attempts; } unsigned int getDownloadAttemptsRemaining() { return _attemptsRemaining; } - virtual void setExtra(void* extra) {}; + virtual void setExtra(void* extra, bool isNewExtra) {}; void setExtraHash(size_t extraHash) { _extraHash = extraHash; } size_t getExtraHash() const { return _extraHash; } From e900d3784be72a9f9576ac9d835b4878e895fe2d Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Feb 2019 23:58:42 +0100 Subject: [PATCH 43/43] fixes --- .../AvatarDoctorErrorReport.qml | 22 ++++++---------- interface/src/avatar/AvatarDoctor.cpp | 25 ++++++++++++------- interface/src/avatar/AvatarDoctor.h | 8 ++---- interface/src/avatar/AvatarPackager.cpp | 2 -- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml index 8811ba48a3..73c5e34d13 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml @@ -21,7 +21,6 @@ Item { height: 40 width: 134 text: qsTr("Try Again") - // colorScheme: root.colorScheme onClicked: { avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; } @@ -49,7 +48,7 @@ Item { color: "#EA4C5F" anchors { top: parent.top - //topMargin: 73 + topMargin: -20 horizontalCenter: parent.horizontalCenter } } @@ -60,7 +59,7 @@ Item { right: parent.right bottom: parent.bottom top: errorReportIcon.bottom - topMargin: 27 + topMargin: -40 leftMargin: 13 rightMargin: 13 } @@ -68,15 +67,6 @@ Item { Repeater { id: errorRepeater - /*model: [ - {message: "Rig is not Hifi compatible", url: "http://www.highfidelity.com/"}, - {message: "Bone limit exceeds 256", url: "http://www.highfidelity.com/2"}, - {message: "Unsupported Texture", url: "http://www.highfidelity.com/texture"}, - {message: "Rig is not Hifi compatible", url: "http://www.highfidelity.com/"}, - {message: "Bone limit exceeds 256", url: "http://www.highfidelity.com/2"}, - {message: "Unsupported Texture", url: "http://www.highfidelity.com/texture"} - ]*/ - Item { height: 37 width: parent.width @@ -89,6 +79,7 @@ Item { anchors { top: parent.top left: parent.left + leftMargin: -5 } } @@ -96,17 +87,18 @@ Item { id: errorLink anchors { top: parent.top + topMargin: 5 left: errorIcon.right right: parent.right } - linkColor: "#00B4EF"// style.colors.blueHighlight + color: "#00B4EF" + linkColor: "#00B4EF" size: 28 text: "" + modelData.message + "" onLinkActivated: Qt.openUrlExternally(modelData.url) + elide: Text.ElideRight } } } } - - } diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index c8f5d52336..b528441be7 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -14,11 +14,22 @@ #include #include + AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : - _avatarFSTFileUrl(std::move(avatarFSTFileUrl)) { + _avatarFSTFileUrl(avatarFSTFileUrl) { + + connect(this, &AvatarDoctor::complete, this, [this](QVariantList errors) { + _isDiagnosing = false; + }); } void AvatarDoctor::startDiagnosing() { + if (_isDiagnosing) { + // One diagnose at a time for now + return; + } + _isDiagnosing = true; + _errors.clear(); _externalTextureCount = 0; @@ -47,8 +58,7 @@ void AvatarDoctor::startDiagnosing() { // RIG if (avatarModel.joints.isEmpty()) { _errors.push_back({ "Avatar has no rig", DEFAULT_URL }); - } - else { + } else { if (avatarModel.joints.length() > 256) { _errors.push_back({ "Avatar has over 256 bones", DEFAULT_URL }); } @@ -69,13 +79,10 @@ void AvatarDoctor::startDiagnosing() { const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; const float avatarHeight = avatarModel.bindExtents.largestDimension(); - - qDebug() << "avatarHeight" << avatarHeight; - qDebug() << "defined Scale =" << model->getMapping()["scale"].toFloat(); if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly smaller then expected.", DEFAULT_URL }); + _errors.push_back({ "Avatar is possibly too small.", DEFAULT_URL }); } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { - _errors.push_back({ "Avatar is possibly larger then expected.", DEFAULT_URL }); + _errors.push_back({ "Avatar is possibly too large.", DEFAULT_URL }); } // TEXTURES @@ -119,7 +126,7 @@ void AvatarDoctor::startDiagnosing() { qDebug() << "checkTextureLoadingComplete" << _checkedTextureCount << "/" << _externalTextureCount; if (_checkedTextureCount == _externalTextureCount) { - if (_missingTextureCount == 1) { + if (_missingTextureCount > 0) { _errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_URL }); } if (_unsupportedTextureCount > 0) { diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index f11bc7377c..bebec32542 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -13,17 +13,11 @@ #ifndef hifi_AvatarDoctor_h #define hifi_AvatarDoctor_h -#include #include #include #include struct AvatarDiagnosticResult { - -//public: - // AvatarDiagnosticResult() {} - // AvatarDiagnosticResult(QString message, QUrl url) : _message(std::move(message)), _url(std::move(url)) { } -//private: QString message; QUrl url; }; @@ -50,6 +44,8 @@ private: int _checkedTextureCount = 0; int _missingTextureCount = 0; int _unsupportedTextureCount = 0; + + bool _isDiagnosing = false; }; #endif // hifi_AvatarDoctor_h diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 24f31cac9c..90def7ad43 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -32,8 +32,6 @@ AvatarPackager::AvatarPackager() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); qRegisterMetaType(); qmlRegisterUncreatableMetaObject( AvatarProjectStatus::staticMetaObject,