From bcd00f98d08253e03fce26baed669b11bcb41afa Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 26 Feb 2019 21:18:32 +0100 Subject: [PATCH] - added more warnings to the avatar doctor - read embedded fst texture mappings --- interface/src/avatar/AvatarDoctor.cpp | 448 ++++++++++++++++++++------ interface/src/avatar/AvatarDoctor.h | 27 +- 2 files changed, 371 insertions(+), 104 deletions(-) diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index b528441be7..66433f005b 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -14,8 +14,61 @@ #include #include +#include -AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : +const int NETWORKED_JOINTS_LIMIT = 256; +const float HIPS_GROUND_MIN_Y = 0.01f; +const float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; +const QString LEFT_JOINT_PREFIX = "Left"; +const QString RIGHT_JOINT_PREFIX = "Right"; + +const QStringList LEG_MAPPING_SUFFIXES = { + "UpLeg" + "Leg", + "Foot", + "ToeBase", +}; + +static QStringList ARM_MAPPING_SUFFIXES = { + "Shoulder", + "Arm", + "ForeArm", + "Hand", +}; + +static QStringList HAND_MAPPING_SUFFIXES = { + "HandIndex3", + "HandIndex2", + "HandIndex1", + "HandPinky3", + "HandPinky2", + "HandPinky1", + "HandMiddle3", + "HandMiddle2", + "HandMiddle1", + "HandRing3", + "HandRing2", + "HandRing1", + "HandThumb3", + "HandThumb2", + "HandThumb1", +}; + +const QUrl DEFAULT_URL = QUrl("https://docs.highfidelity.com/create/avatars/create-avatars.html#create-your-own-avatar"); + +DiagnosableAvatar::DiagnosableAvatar(QThread* thread) : Avatar(thread) { + // give the pointer to our head to inherited _headData variable from AvatarData + _headData = new Head(this); + _skeletonModel = std::make_shared(this, nullptr); + _skeletonModel->setLoadingPriority(MYAVATAR_LOADING_PRIORITY); + connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); + connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); + connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); +} + +DiagnosableAvatar::~DiagnosableAvatar() = default; + +AvatarDoctor::AvatarDoctor(const QUrl& avatarFSTFileUrl) : _avatarFSTFileUrl(avatarFSTFileUrl) { connect(this, &AvatarDoctor::complete, this, [this](QVariantList errors) { @@ -39,136 +92,206 @@ void AvatarDoctor::startDiagnosing() { const auto resource = DependencyManager::get()->getGeometryResource(_avatarFSTFileUrl); 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) { + + const auto resourceLoaded = [this, resource](bool success) { // MODEL if (!success) { - _errors.push_back({ "Model file cannot be opened", DEFAULT_URL }); + _errors.push_back({ "Model file cannot be opened.", DEFAULT_URL }); emit complete(getErrors()); return; } + _model = resource; const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); if (!avatarModel.originalURL.endsWith(".fbx")) { - _errors.push_back({ "Unsupported avatar model format", DEFAULT_URL }); + _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", DEFAULT_URL }); + _errors.push_back({ "Avatar has no rig.", DEFAULT_URL }); } else { - if (avatarModel.joints.length() > 256) { - _errors.push_back({ "Avatar has over 256 bones", DEFAULT_URL }); + auto jointNames = avatarModel.getJointNames(); + + if (avatarModel.joints.length() > NETWORKED_JOINTS_LIMIT) { + _errors.push_back({tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), DEFAULT_URL }); } // Avatar does not have Hips bone mapped - if (!avatarModel.getJointNames().contains("Hips")) { - _errors.push_back({ "Hips are not mapped", DEFAULT_URL }); + if (!jointNames.contains("Hips")) { + _errors.push_back({ "Hips are not mapped.", DEFAULT_URL }); } - if (!avatarModel.getJointNames().contains("Spine")) { - _errors.push_back({ "Spine is not mapped", DEFAULT_URL }); + if (!jointNames.contains("Spine")) { + _errors.push_back({ "Spine is not mapped.", DEFAULT_URL }); } - if (!avatarModel.getJointNames().contains("Head")) { - _errors.push_back({ "Head is not mapped", DEFAULT_URL }); + if (!jointNames.contains("Spine1")) { + _errors.push_back({ "Chest (Spine1) is not mapped.", DEFAULT_URL }); + } + if (!jointNames.contains("Neck")) { + _errors.push_back({ "Neck is not mapped.", DEFAULT_URL }); + } + if (!jointNames.contains("Head")) { + _errors.push_back({ "Head is not mapped.", DEFAULT_URL }); + } + + if (!jointNames.contains("LeftEye")) { + if (jointNames.contains("RightEye")) { + _errors.push_back({ "LeftEye is not mapped.", DEFAULT_URL }); + } else { + _errors.push_back({ "Eyes are not mapped.", DEFAULT_URL }); + } + } else if (!jointNames.contains("RightEye")) { + _errors.push_back({ "RightEye is not mapped.", DEFAULT_URL }); + } + + const auto checkJointAsymmetry = [jointNames] (const QStringList& jointMappingSuffixes) { + foreach (const QString& jointSuffix, jointMappingSuffixes) { + if (jointNames.contains(LEFT_JOINT_PREFIX + jointSuffix) != + jointNames.contains(RIGHT_JOINT_PREFIX + jointSuffix)) { + return true; + } + } + return false; + }; + + const auto isDescendantOfJointWhenJointsExist = [avatarModel, jointNames] (const QString& jointName, const QString& ancestorName) { + if (!jointNames.contains(jointName) || !jointNames.contains(ancestorName)) { + return true; + } + auto currentJoint = avatarModel.joints[avatarModel.jointIndices[jointName] - 1]; + while (currentJoint.parentIndex != -1) { + currentJoint = avatarModel.joints[currentJoint.parentIndex]; + if (currentJoint.name == ancestorName) { + return true; + } + } + return false; + }; + + if (checkJointAsymmetry(ARM_MAPPING_SUFFIXES)) { + _errors.push_back({ "Asymmetrical arm bones.", DEFAULT_URL }); + } + if (checkJointAsymmetry(HAND_MAPPING_SUFFIXES)) { + _errors.push_back({ "Asymmetrical hand bones.", DEFAULT_URL }); + } + if (checkJointAsymmetry(LEG_MAPPING_SUFFIXES)) { + _errors.push_back({ "Asymmetrical leg bones.", DEFAULT_URL }); + } + _avatar = new DiagnosableAvatar(QThread::currentThread()); + + _avatar->setSkeletonModelURL(_avatarFSTFileUrl); + if (_avatar->getSkeletonModel()->updateGeometry()) { + // Rig has been fully loaded + + // SCALE + const float RECOMMENDED_MIN_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; + + const float avatarHeight = _avatar->getHeight(); + if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { + _errors.push_back({ "Avatar is possibly too short.", DEFAULT_URL }); + } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { + _errors.push_back({ "Avatar is possibly too tall.", DEFAULT_URL }); + } + + auto rig = &_avatar->getSkeletonModel()->getRig(); + + // HipsNotOnGround + auto hipsIndex = rig->indexOfJoint("Hips"); + if (hipsIndex >= 0) { + glm::vec3 hipsPosition; + if (rig->getJointPosition(hipsIndex, hipsPosition)) { + const auto hipJoint = avatarModel.joints.at(avatarModel.getJointIndex("Hips")); + + if (hipsPosition.y < HIPS_GROUND_MIN_Y) { + _errors.push_back({ "Hips are on ground.", DEFAULT_URL }); + } + } + } + + // HipsSpineChestNotCoincident + auto spineIndex = rig->indexOfJoint("Spine"); + auto chestIndex = rig->indexOfJoint("Spine1"); + if (hipsIndex >= 0 && spineIndex >= 0 && chestIndex >= 0) { + glm::vec3 hipsPosition; + glm::vec3 spinePosition; + glm::vec3 chestPosition; + if (rig->getJointPosition(hipsIndex, hipsPosition) && + rig->getJointPosition(spineIndex, spinePosition) && + rig->getJointPosition(chestIndex, chestPosition)) { + + const auto hipsToSpine = glm::length(hipsPosition - spinePosition); + const auto spineToChest = glm::length(spinePosition - chestPosition); + if (hipsToSpine < HIPS_SPINE_CHEST_MIN_SEPARATION && spineToChest < HIPS_SPINE_CHEST_MIN_SEPARATION) { + _errors.push_back({ "Hips/Spine/Chest overlap.", DEFAULT_URL }); + } + } + } + } + _avatar->deleteLater(); + _avatar = nullptr; + + auto mapping = resource->getMapping(); + + if (mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { + const auto& jointNameMappings = mapping[JOINT_NAME_MAPPING_FIELD].toHash(); + QStringList jointValues; + foreach(const auto& jointVariant, jointNameMappings.values()) { + jointValues << jointVariant.toString(); + } + + const auto& uniqueJointValues = jointValues.toSet(); + foreach (const auto& jointName, uniqueJointValues) { + if (jointValues.count(jointName) > 1) { + _errors.push_back({ tr("%1 is mapped multiple times.").arg(jointName), DEFAULT_URL }); + } + } + } + + if (!isDescendantOfJointWhenJointsExist("Spine", "Hips")) { + _errors.push_back({ "Spine is no child of Hips.", DEFAULT_URL }); + } + + if (!isDescendantOfJointWhenJointsExist("Spine1", "Spine")) { + _errors.push_back({ "Spine1 is no child of Spine.", DEFAULT_URL }); + } + + if (!isDescendantOfJointWhenJointsExist("Head", "Spine1")) { + _errors.push_back({ "Head is no child of Spine1.", DEFAULT_URL }); } } - // SCALE - 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(); - if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly too small.", DEFAULT_URL }); - } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { - _errors.push_back({ "Avatar is possibly too large.", DEFAULT_URL }); - } - - // TEXTURES - 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; + auto materialMappingHandled = [this]() mutable { + _materialMappingLoadedCount++; + // Continue diagnosing the textures as soon as the material mappings have tried to load. + if (_materialMappingLoadedCount == _materialMappingCount) { + // TEXTURES + diagnoseTextures(); } }; - - 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; - } - - 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 > 0) { - _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); - } + _materialMappingCount = (int)model->getMaterialMapping().size(); + _materialMappingLoadedCount = 0; + foreach(const auto& materialMapping, model->getMaterialMapping()) { + // refresh the texture mappings + auto materialMappingResource = materialMapping.second; + if (materialMappingResource) { + materialMappingResource->refresh(); + if (materialMappingResource->isLoaded()) { + materialMappingHandled(); } else { - _missingTextureCount++; - _checkedTextureCount++; - checkTextureLoadingComplete(); + connect(materialMappingResource.data(), &NetworkTexture::finished, this, [materialMappingHandled](bool success) mutable { + materialMappingHandled(); + }); } + } else { + materialMappingHandled(); } - } else { - emit complete(getErrors()); + } + if (_materialMappingCount == 0) { + // TEXTURES + diagnoseTextures(); } }; @@ -184,6 +307,125 @@ void AvatarDoctor::startDiagnosing() { } } +void AvatarDoctor::diagnoseTextures() { + const auto model = _model.data(); + const auto avatarModel = _model.data()->getHFMModel(); + QStringList externalTextures{}; + QSet textureNames{}; + int texturesFound = 0; + auto addTextureToList = [&externalTextures, &texturesFound](hfm::Texture texture) mutable { + if (texture.filename.isEmpty()) { + return; + } + if (texture.content.isEmpty() && !externalTextures.contains(texture.name)) { + externalTextures << texture.name; + } + texturesFound++; + }; + + 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); + } + + foreach(const auto& materialMapping, model->getMaterialMapping()) { + foreach(const auto& networkMaterial, materialMapping.second.data()->parsedMaterials.networkMaterials) { + foreach(const auto& textureMap, networkMaterial.second->getTextureMaps()) { + texturesFound++; + } + } + } + + auto normalizedURL = DependencyManager::get()->normalizeURL( + QUrl(avatarModel.originalURL)).resolved(QUrl("textures")); + + bool isTextureFolderEmpty = true; + if (normalizedURL.isLocalFile()) { + QDir texturesFolder(normalizedURL.toLocalFile()); + if (texturesFolder.exists()) { + isTextureFolderEmpty = texturesFolder.isEmpty(); + } + } + + if (texturesFound == 0 && !isTextureFolderEmpty) { + _errors.push_back({ tr("No textures assigned."), DEFAULT_URL }); + } + + 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; + } + + const QUrl textureURL = modelTexturesURLs[textureKey].toUrl(); + + auto textureResource = DependencyManager::get()->getTexture(textureURL); + auto checkTextureLoadingComplete = [this]() mutable { + qDebug() << "checkTextureLoadingComplete" << _checkedTextureCount << "/" << _externalTextureCount; + + if (_checkedTextureCount == _externalTextureCount) { + if (_missingTextureCount > 0) { + _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()); + } +} + QVariantList AvatarDoctor::getErrors() const { QVariantList result; for (const auto& error : _errors) { diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index bebec32542..17397d99df 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -16,6 +16,7 @@ #include #include #include +#include struct AvatarDiagnosticResult { QString message; @@ -24,10 +25,25 @@ struct AvatarDiagnosticResult { Q_DECLARE_METATYPE(AvatarDiagnosticResult) Q_DECLARE_METATYPE(QVector) + +class DiagnosableAvatar: public Avatar { +public: + explicit DiagnosableAvatar(QThread* thread); + virtual ~DiagnosableAvatar(); + + void simulate(float deltaTime, bool inView) override { + + } + void rebuildCollisionShape() override { + + } + virtual void instantiableAvatar() override { }; +}; + class AvatarDoctor : public QObject { Q_OBJECT public: - AvatarDoctor(QUrl avatarFSTFileUrl); + AvatarDoctor(const QUrl& avatarFSTFileUrl); Q_INVOKABLE void startDiagnosing(); @@ -37,6 +53,8 @@ signals: void complete(QVariantList errors); private: + void diagnoseTextures(); + QUrl _avatarFSTFileUrl; QVector _errors; @@ -45,6 +63,13 @@ private: int _missingTextureCount = 0; int _unsupportedTextureCount = 0; + int _materialMappingCount = 0; + int _materialMappingLoadedCount = 0; + + DiagnosableAvatar* _avatar { nullptr }; + + GeometryResource::Pointer _model; + bool _isDiagnosing = false; };