//// AvatarDoctor.cpp//// Created by Thijs Wenker on 2/12/2019.// Copyright 2019 High Fidelity, Inc.// Copyright 2021 Vircadia contributors.//// 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 <model-networking/ModelCache.h>#include <AvatarConstants.h>#include <Rig.h>#include <ResourceManager.h>#include <QDir>#include <FSTReader.h> 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 PACKAGE_AVATAR_DOCS_BASE_URL = QUrl("https://docs.overte.org/create/avatars/package-avatar.html"); AvatarDoctor::AvatarDoctor(const QUrl& 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; _checkedTextureCount = 0; _missingTextureCount = 0; _unsupportedTextureCount = 0; const auto resource = DependencyManager::get<ModelCache>()->getGeometryResource(_avatarFSTFileUrl); resource->refresh(); const auto resourceLoaded = [this, resource](bool success) { // MODEL if (!success) { addError("Model file cannot be opened.", "missing-file"); emit complete(getErrors()); return; } _model = resource; const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); if (!avatarModel.originalURL.toLower().endsWith(".fbx")) { addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); return; } // RIG if (avatarModel.joints.isEmpty()) { addError("Avatar has no rig.", "no-rig"); } else { auto jointNames = avatarModel.getJointNames(); if (avatarModel.joints.length() > NETWORKED_JOINTS_LIMIT) { addError(tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), "maximum-bone-limit"); } // Avatar does not have Hips bone mapped if (!jointNames.contains("Hips")) { addError("Hips are not mapped.", "hips-not-mapped"); } if (!jointNames.contains("Spine")) { addError("Spine is not mapped.", "spine-not-mapped"); } if (!jointNames.contains("Spine1")) { addError("Chest (Spine1) is not mapped.", "chest-not-mapped"); } if (!jointNames.contains("Neck")) { addError("Neck is not mapped.", "neck-not-mapped"); } if (!jointNames.contains("Head")) { addError("Head is not mapped.", "head-not-mapped"); } if (!jointNames.contains("LeftEye")) { if (jointNames.contains("RightEye")) { addError("LeftEye is not mapped.", "eye-not-mapped"); } else { addError("Eyes are not mapped.", "eye-not-mapped"); } } else if (!jointNames.contains("RightEye")) { addError("RightEye is not mapped.", "eye-not-mapped"); } const auto checkJointAsymmetry = [jointNames] (const QStringList& jointMappingSuffixes) { for (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)) { addError("Asymmetrical arm bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(HAND_MAPPING_SUFFIXES)) { addError("Asymmetrical hand bones.", "asymmetrical-bones"); } if (checkJointAsymmetry(LEG_MAPPING_SUFFIXES)) { addError("Asymmetrical leg bones.", "asymmetrical-bones"); } // Multiple skeleton root joints checkup int skeletonRootJoints = 0; for (const HFMJoint& joint: avatarModel.joints) { if (joint.parentIndex == -1 && joint.isSkeletonJoint) { skeletonRootJoints++; } } if (skeletonRootJoints > 1) { addError("Multiple top-level joints found.", "multiple-top-level-joints"); } Rig rig; rig.reset(avatarModel); const float eyeHeight = rig.getUnscaledEyeHeight(); const float ratio = eyeHeight / DEFAULT_AVATAR_HEIGHT; const float avatarHeight = eyeHeight + ratio * DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD; // SCALE const float RECOMMENDED_MIN_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { addError("Avatar is possibly too short.", "short-avatar"); } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { addError("Avatar is possibly too tall.", "tall-avatar"); } // 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) { addError("Hips are on ground.", "hips-on-ground"); } } } // 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) { addError("Hips/Spine/Chest overlap.", "overlap-error"); } } } 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; for (const auto& jointVariant: jointNameMappings.values()) { jointValues << jointVariant.toString(); } const auto& uniqueJointValues = QSet<QString>(jointValues.begin(), jointValues.end()); for (const auto& jointName: uniqueJointValues) { if (jointValues.count(jointName) > 1) { addError(tr("%1 is mapped multiple times.").arg(jointName), "mapped-multiple-times"); } } } if (!isDescendantOfJointWhenJointsExist("Spine", "Hips")) { addError("Spine is not a child of Hips.", "spine-not-child"); } if (!isDescendantOfJointWhenJointsExist("Spine1", "Spine")) { addError("Spine1 is not a child of Spine.", "spine1-not-child"); } if (!isDescendantOfJointWhenJointsExist("Head", "Spine1")) { addError("Head is not a child of Spine1.", "head-not-child"); } } auto materialMappingHandled = [this]() mutable { _materialMappingLoadedCount++; // Continue diagnosing the textures as soon as the material mappings have tried to load. if (_materialMappingLoadedCount == _materialMappingCount) { // TEXTURES diagnoseTextures(); } }; _materialMappingCount = (int)model->getMaterialMapping().size(); _materialMappingLoadedCount = 0; for (const auto& materialMapping : model->getMaterialMapping()) { // refresh the texture mappings auto materialMappingResource = materialMapping.second; if (materialMappingResource) { materialMappingResource->refresh(); if (materialMappingResource->isLoaded()) { materialMappingHandled(); } else { connect(materialMappingResource.data(), &NetworkTexture::finished, this, [materialMappingHandled](bool success) mutable { materialMappingHandled(); }); } } else { materialMappingHandled(); } } if (_materialMappingCount == 0) { // TEXTURES diagnoseTextures(); } }; if (resource) { if (resource->isLoaded()) { resourceLoaded(!resource->isFailed()); } else { connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); } } else { addError("Model file cannot be opened", "missing-file"); emit complete(getErrors()); }} void AvatarDoctor::diagnoseTextures() { const auto model = _model.data(); const auto avatarModel = _model.data()->getHFMModel(); QVector<QString> externalTextures{}; QVector<QString> 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++; }; for (const auto& 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 (material.isMToonMaterial) { addTextureToList(material.shadeTexture); addTextureToList(material.shadingShiftTexture); addTextureToList(material.matcapTexture); addTextureToList(material.rimTexture); addTextureToList(material.uvAnimationTexture); } } for (const auto& materialMapping : model->getMaterialMapping()) { for (const auto& networkMaterial : materialMapping.second.data()->parsedMaterials.networkMaterials) { texturesFound += (int)networkMaterial.second->getTextureMaps().size(); } } auto normalizedURL = DependencyManager::get<ResourceManager>()->normalizeURL( QUrl(avatarModel.originalURL)).resolved(QUrl("textures")); if (texturesFound == 0) { addError(tr("No textures assigned."), "no-textures-assigned"); } if (!externalTextures.empty()) { // Check External Textures: auto modelTexturesURLs = model->getTextures(); _externalTextureCount = externalTextures.length(); auto checkTextureLoadingComplete = [this]() mutable { if (_checkedTextureCount == _externalTextureCount) { if (_missingTextureCount > 0) { addError(tr("Missing %n texture(s).","", _missingTextureCount), "missing-textures"); } if (_unsupportedTextureCount > 0) { addError(tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), "unsupported-textures"); } emit complete(getErrors()); } }; for (const QString& textureKey : externalTextures) { if (!modelTexturesURLs.contains(textureKey)) { _missingTextureCount++; _checkedTextureCount++; continue; } const QUrl textureURL = modelTexturesURLs[textureKey].toUrl(); auto textureResource = DependencyManager::get<TextureCache>()->getTexture(textureURL); auto textureLoaded = [this, textureResource, checkTextureLoadingComplete](bool success) mutable { if (!success) { auto normalizedURL = DependencyManager::get<ResourceManager>()->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()); }} void AvatarDoctor::addError(const QString& errorMessage, const QString& docFragment) { QUrl documentationURL = PACKAGE_AVATAR_DOCS_BASE_URL; documentationURL.setFragment(docFragment); _errors.push_back({ errorMessage, documentationURL });} 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;}
//
// AvatarDoctor.cpp
//
// Created by Thijs Wenker on 2/12/2019.
// Copyright 2019 High Fidelity, Inc.
// Copyright 2021 Vircadia contributors.
//
// 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 <model-networking/ModelCache.h>
#include <AvatarConstants.h>
#include <Rig.h>
#include <ResourceManager.h>
#include <QDir>
#include <FSTReader.h>
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 PACKAGE_AVATAR_DOCS_BASE_URL = QUrl("https://docs.overte.org/create/avatars/package-avatar.html");
AvatarDoctor::AvatarDoctor(const QUrl& 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;
_checkedTextureCount = 0;
_missingTextureCount = 0;
_unsupportedTextureCount = 0;
const auto resource = DependencyManager::get<ModelCache>()->getGeometryResource(_avatarFSTFileUrl);
resource->refresh();
const auto resourceLoaded = [this, resource](bool success) {
// MODEL
if (!success) {
addError("Model file cannot be opened.", "missing-file");
emit complete(getErrors());
return;
}
_model = resource;
const auto model = resource.data();
const auto avatarModel = resource.data()->getHFMModel();
if (!avatarModel.originalURL.toLower().endsWith(".fbx")) {
addError("Unsupported avatar model format.", "unsupported-format");
emit complete(getErrors());
return;
}
// RIG
if (avatarModel.joints.isEmpty()) {
addError("Avatar has no rig.", "no-rig");
} else {
auto jointNames = avatarModel.getJointNames();
if (avatarModel.joints.length() > NETWORKED_JOINTS_LIMIT) {
addError(tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), "maximum-bone-limit");
}
// Avatar does not have Hips bone mapped
if (!jointNames.contains("Hips")) {
addError("Hips are not mapped.", "hips-not-mapped");
}
if (!jointNames.contains("Spine")) {
addError("Spine is not mapped.", "spine-not-mapped");
}
if (!jointNames.contains("Spine1")) {
addError("Chest (Spine1) is not mapped.", "chest-not-mapped");
}
if (!jointNames.contains("Neck")) {
addError("Neck is not mapped.", "neck-not-mapped");
}
if (!jointNames.contains("Head")) {
addError("Head is not mapped.", "head-not-mapped");
}
if (!jointNames.contains("LeftEye")) {
if (jointNames.contains("RightEye")) {
addError("LeftEye is not mapped.", "eye-not-mapped");
} else {
addError("Eyes are not mapped.", "eye-not-mapped");
}
} else if (!jointNames.contains("RightEye")) {
addError("RightEye is not mapped.", "eye-not-mapped");
}
const auto checkJointAsymmetry = [jointNames] (const QStringList& jointMappingSuffixes) {
for (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)) {
addError("Asymmetrical arm bones.", "asymmetrical-bones");
}
if (checkJointAsymmetry(HAND_MAPPING_SUFFIXES)) {
addError("Asymmetrical hand bones.", "asymmetrical-bones");
}
if (checkJointAsymmetry(LEG_MAPPING_SUFFIXES)) {
addError("Asymmetrical leg bones.", "asymmetrical-bones");
}
// Multiple skeleton root joints checkup
int skeletonRootJoints = 0;
for (const HFMJoint& joint: avatarModel.joints) {
if (joint.parentIndex == -1 && joint.isSkeletonJoint) {
skeletonRootJoints++;
}
}
if (skeletonRootJoints > 1) {
addError("Multiple top-level joints found.", "multiple-top-level-joints");
}
Rig rig;
rig.reset(avatarModel);
const float eyeHeight = rig.getUnscaledEyeHeight();
const float ratio = eyeHeight / DEFAULT_AVATAR_HEIGHT;
const float avatarHeight = eyeHeight + ratio * DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD;
// SCALE
const float RECOMMENDED_MIN_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f;
const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f;
if (avatarHeight < RECOMMENDED_MIN_HEIGHT) {
addError("Avatar is possibly too short.", "short-avatar");
} else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) {
addError("Avatar is possibly too tall.", "tall-avatar");
}
// 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) {
addError("Hips are on ground.", "hips-on-ground");
}
}
}
// 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) {
addError("Hips/Spine/Chest overlap.", "overlap-error");
}
}
}
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;
for (const auto& jointVariant: jointNameMappings.values()) {
jointValues << jointVariant.toString();
}
const auto& uniqueJointValues = QSet<QString>(jointValues.begin(), jointValues.end());
for (const auto& jointName: uniqueJointValues) {
if (jointValues.count(jointName) > 1) {
addError(tr("%1 is mapped multiple times.").arg(jointName), "mapped-multiple-times");
}
}
}
if (!isDescendantOfJointWhenJointsExist("Spine", "Hips")) {
addError("Spine is not a child of Hips.", "spine-not-child");
}
if (!isDescendantOfJointWhenJointsExist("Spine1", "Spine")) {
addError("Spine1 is not a child of Spine.", "spine1-not-child");
}
if (!isDescendantOfJointWhenJointsExist("Head", "Spine1")) {
addError("Head is not a child of Spine1.", "head-not-child");
}
}
auto materialMappingHandled = [this]() mutable {
_materialMappingLoadedCount++;
// Continue diagnosing the textures as soon as the material mappings have tried to load.
if (_materialMappingLoadedCount == _materialMappingCount) {
// TEXTURES
diagnoseTextures();
}
};
_materialMappingCount = (int)model->getMaterialMapping().size();
_materialMappingLoadedCount = 0;
for (const auto& materialMapping : model->getMaterialMapping()) {
// refresh the texture mappings
auto materialMappingResource = materialMapping.second;
if (materialMappingResource) {
materialMappingResource->refresh();
if (materialMappingResource->isLoaded()) {
materialMappingHandled();
} else {
connect(materialMappingResource.data(), &NetworkTexture::finished, this,
[materialMappingHandled](bool success) mutable {
materialMappingHandled();
});
}
} else {
materialMappingHandled();
}
}
if (_materialMappingCount == 0) {
// TEXTURES
diagnoseTextures();
}
};
if (resource) {
if (resource->isLoaded()) {
resourceLoaded(!resource->isFailed());
} else {
connect(resource.data(), &GeometryResource::finished, this, resourceLoaded);
}
} else {
addError("Model file cannot be opened", "missing-file");
emit complete(getErrors());
}
}
void AvatarDoctor::diagnoseTextures() {
const auto model = _model.data();
const auto avatarModel = _model.data()->getHFMModel();
QVector<QString> externalTextures{};
QVector<QString> 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++;
};
for (const auto& 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 (material.isMToonMaterial) {
addTextureToList(material.shadeTexture);
addTextureToList(material.shadingShiftTexture);
addTextureToList(material.matcapTexture);
addTextureToList(material.rimTexture);
addTextureToList(material.uvAnimationTexture);
}
}
for (const auto& materialMapping : model->getMaterialMapping()) {
for (const auto& networkMaterial : materialMapping.second.data()->parsedMaterials.networkMaterials) {
texturesFound += (int)networkMaterial.second->getTextureMaps().size();
}
}
auto normalizedURL = DependencyManager::get<ResourceManager>()->normalizeURL(
QUrl(avatarModel.originalURL)).resolved(QUrl("textures"));
if (texturesFound == 0) {
addError(tr("No textures assigned."), "no-textures-assigned");
}
if (!externalTextures.empty()) {
// Check External Textures:
auto modelTexturesURLs = model->getTextures();
_externalTextureCount = externalTextures.length();
auto checkTextureLoadingComplete = [this]() mutable {
if (_checkedTextureCount == _externalTextureCount) {
if (_missingTextureCount > 0) {
addError(tr("Missing %n texture(s).","", _missingTextureCount), "missing-textures");
}
if (_unsupportedTextureCount > 0) {
addError(tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), "unsupported-textures");
}
emit complete(getErrors());
}
};
for (const QString& textureKey : externalTextures) {
if (!modelTexturesURLs.contains(textureKey)) {
_missingTextureCount++;
_checkedTextureCount++;
continue;
}
const QUrl textureURL = modelTexturesURLs[textureKey].toUrl();
auto textureResource = DependencyManager::get<TextureCache>()->getTexture(textureURL);
auto textureLoaded = [this, textureResource, checkTextureLoadingComplete](bool success) mutable {
if (!success) {
auto normalizedURL = DependencyManager::get<ResourceManager>()->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());
}
}
void AvatarDoctor::addError(const QString& errorMessage, const QString& docFragment) {
QUrl documentationURL = PACKAGE_AVATAR_DOCS_BASE_URL;
documentationURL.setFragment(docFragment);
_errors.push_back({ errorMessage, documentationURL });
}
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;
}