mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 22:28:37 +02:00
Merge pull request #15035 from thoys/feat/avatarTools/avatarDoctorMoreWarnings
Case 21449: [AvatarPackager/AvatarDoctor] Warnings update
This commit is contained in:
commit
60d46998ec
5 changed files with 393 additions and 153 deletions
|
@ -12,10 +12,52 @@
|
||||||
#include "AvatarDoctor.h"
|
#include "AvatarDoctor.h"
|
||||||
#include <model-networking/ModelCache.h>
|
#include <model-networking/ModelCache.h>
|
||||||
#include <AvatarConstants.h>
|
#include <AvatarConstants.h>
|
||||||
|
#include <Rig.h>
|
||||||
#include <ResourceManager.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";
|
||||||
|
|
||||||
AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) :
|
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_DOCS_URL = QUrl("https://docs.highfidelity.com/create/avatars/create-avatars.html#create-your-own-avatar");
|
||||||
|
|
||||||
|
AvatarDoctor::AvatarDoctor(const QUrl& avatarFSTFileUrl) :
|
||||||
_avatarFSTFileUrl(avatarFSTFileUrl) {
|
_avatarFSTFileUrl(avatarFSTFileUrl) {
|
||||||
|
|
||||||
connect(this, &AvatarDoctor::complete, this, [this](QVariantList errors) {
|
connect(this, &AvatarDoctor::complete, this, [this](QVariantList errors) {
|
||||||
|
@ -39,136 +81,215 @@ void AvatarDoctor::startDiagnosing() {
|
||||||
|
|
||||||
const auto resource = DependencyManager::get<ModelCache>()->getGeometryResource(_avatarFSTFileUrl);
|
const auto resource = DependencyManager::get<ModelCache>()->getGeometryResource(_avatarFSTFileUrl);
|
||||||
resource->refresh();
|
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
|
// MODEL
|
||||||
if (!success) {
|
if (!success) {
|
||||||
_errors.push_back({ "Model file cannot be opened", DEFAULT_URL });
|
_errors.push_back({ "Model file cannot be opened.", DEFAULT_DOCS_URL });
|
||||||
emit complete(getErrors());
|
emit complete(getErrors());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_model = resource;
|
||||||
const auto model = resource.data();
|
const auto model = resource.data();
|
||||||
const auto avatarModel = resource.data()->getHFMModel();
|
const auto avatarModel = resource.data()->getHFMModel();
|
||||||
if (!avatarModel.originalURL.endsWith(".fbx")) {
|
if (!avatarModel.originalURL.endsWith(".fbx")) {
|
||||||
_errors.push_back({ "Unsupported avatar model format", DEFAULT_URL });
|
_errors.push_back({ "Unsupported avatar model format.", DEFAULT_DOCS_URL });
|
||||||
emit complete(getErrors());
|
emit complete(getErrors());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RIG
|
// RIG
|
||||||
if (avatarModel.joints.isEmpty()) {
|
if (avatarModel.joints.isEmpty()) {
|
||||||
_errors.push_back({ "Avatar has no rig", DEFAULT_URL });
|
_errors.push_back({ "Avatar has no rig.", DEFAULT_DOCS_URL });
|
||||||
} else {
|
} else {
|
||||||
if (avatarModel.joints.length() > 256) {
|
auto jointNames = avatarModel.getJointNames();
|
||||||
_errors.push_back({ "Avatar has over 256 bones", DEFAULT_URL });
|
|
||||||
|
if (avatarModel.joints.length() > NETWORKED_JOINTS_LIMIT) {
|
||||||
|
_errors.push_back({tr( "Avatar has over %n bones.", "", NETWORKED_JOINTS_LIMIT), DEFAULT_DOCS_URL });
|
||||||
}
|
}
|
||||||
// Avatar does not have Hips bone mapped
|
// Avatar does not have Hips bone mapped
|
||||||
if (!avatarModel.getJointNames().contains("Hips")) {
|
if (!jointNames.contains("Hips")) {
|
||||||
_errors.push_back({ "Hips are not mapped", DEFAULT_URL });
|
_errors.push_back({ "Hips are not mapped.", DEFAULT_DOCS_URL });
|
||||||
}
|
}
|
||||||
if (!avatarModel.getJointNames().contains("Spine")) {
|
if (!jointNames.contains("Spine")) {
|
||||||
_errors.push_back({ "Spine is not mapped", DEFAULT_URL });
|
_errors.push_back({ "Spine is not mapped.", DEFAULT_DOCS_URL });
|
||||||
}
|
}
|
||||||
if (!avatarModel.getJointNames().contains("Head")) {
|
if (!jointNames.contains("Spine1")) {
|
||||||
_errors.push_back({ "Head is not mapped", DEFAULT_URL });
|
_errors.push_back({ "Chest (Spine1) is not mapped.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
if (!jointNames.contains("Neck")) {
|
||||||
|
_errors.push_back({ "Neck is not mapped.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
if (!jointNames.contains("Head")) {
|
||||||
|
_errors.push_back({ "Head is not mapped.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jointNames.contains("LeftEye")) {
|
||||||
|
if (jointNames.contains("RightEye")) {
|
||||||
|
_errors.push_back({ "LeftEye is not mapped.", DEFAULT_DOCS_URL });
|
||||||
|
} else {
|
||||||
|
_errors.push_back({ "Eyes are not mapped.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
} else if (!jointNames.contains("RightEye")) {
|
||||||
|
_errors.push_back({ "RightEye is not mapped.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
_errors.push_back({ "Asymmetrical arm bones.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
if (checkJointAsymmetry(HAND_MAPPING_SUFFIXES)) {
|
||||||
|
_errors.push_back({ "Asymmetrical hand bones.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
if (checkJointAsymmetry(LEG_MAPPING_SUFFIXES)) {
|
||||||
|
_errors.push_back({ "Asymmetrical leg bones.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple skeleton root joints checkup
|
||||||
|
int skeletonRootJoints = 0;
|
||||||
|
for (const HFMJoint& joint: avatarModel.joints) {
|
||||||
|
if (joint.parentIndex == -1 && joint.isSkeletonJoint) {
|
||||||
|
skeletonRootJoints++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skeletonRootJoints > 1) {
|
||||||
|
_errors.push_back({ "Multiple top-level joints found.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
_errors.push_back({ "Avatar is possibly too short.", DEFAULT_DOCS_URL });
|
||||||
|
} else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) {
|
||||||
|
_errors.push_back({ "Avatar is possibly too tall.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_DOCS_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_DOCS_URL });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = jointValues.toSet();
|
||||||
|
for (const auto& jointName: uniqueJointValues) {
|
||||||
|
if (jointValues.count(jointName) > 1) {
|
||||||
|
_errors.push_back({ tr("%1 is mapped multiple times.").arg(jointName), DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDescendantOfJointWhenJointsExist("Spine", "Hips")) {
|
||||||
|
_errors.push_back({ "Spine is not a child of Hips.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDescendantOfJointWhenJointsExist("Spine1", "Spine")) {
|
||||||
|
_errors.push_back({ "Spine1 is not a child of Spine.", DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDescendantOfJointWhenJointsExist("Head", "Spine1")) {
|
||||||
|
_errors.push_back({ "Head is not a child of Spine1.", DEFAULT_DOCS_URL });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SCALE
|
auto materialMappingHandled = [this]() mutable {
|
||||||
const float RECOMMENDED_MIN_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f;
|
_materialMappingLoadedCount++;
|
||||||
const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f;
|
// Continue diagnosing the textures as soon as the material mappings have tried to load.
|
||||||
|
if (_materialMappingLoadedCount == _materialMappingCount) {
|
||||||
const float avatarHeight = avatarModel.bindExtents.largestDimension();
|
// TEXTURES
|
||||||
if (avatarHeight < RECOMMENDED_MIN_HEIGHT) {
|
diagnoseTextures();
|
||||||
_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<QString> 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()) {
|
_materialMappingCount = (int)model->getMaterialMapping().size();
|
||||||
// Check External Textures:
|
_materialMappingLoadedCount = 0;
|
||||||
auto modelTexturesURLs = model->getTextures();
|
for (const auto& materialMapping : model->getMaterialMapping()) {
|
||||||
_externalTextureCount = externalTextures.length();
|
// refresh the texture mappings
|
||||||
foreach(const QString textureKey, externalTextures) {
|
auto materialMappingResource = materialMapping.second;
|
||||||
if (!modelTexturesURLs.contains(textureKey)) {
|
if (materialMappingResource) {
|
||||||
_missingTextureCount++;
|
materialMappingResource->refresh();
|
||||||
_checkedTextureCount++;
|
if (materialMappingResource->isLoaded()) {
|
||||||
continue;
|
materialMappingHandled();
|
||||||
}
|
|
||||||
|
|
||||||
const QUrl textureURL = modelTexturesURLs[textureKey].toUrl();
|
|
||||||
|
|
||||||
auto textureResource = DependencyManager::get<TextureCache>()->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<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 {
|
} else {
|
||||||
_missingTextureCount++;
|
connect(materialMappingResource.data(), &NetworkTexture::finished, this,
|
||||||
_checkedTextureCount++;
|
[materialMappingHandled](bool success) mutable {
|
||||||
checkTextureLoadingComplete();
|
|
||||||
|
materialMappingHandled();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
materialMappingHandled();
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
emit complete(getErrors());
|
if (_materialMappingCount == 0) {
|
||||||
|
// TEXTURES
|
||||||
|
diagnoseTextures();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -179,11 +300,117 @@ void AvatarDoctor::startDiagnosing() {
|
||||||
connect(resource.data(), &GeometryResource::finished, this, resourceLoaded);
|
connect(resource.data(), &GeometryResource::finished, this, resourceLoaded);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_errors.push_back({ "Model file cannot be opened", DEFAULT_URL });
|
_errors.push_back({ "Model file cannot be opened", DEFAULT_DOCS_URL });
|
||||||
emit complete(getErrors());
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
_errors.push_back({ tr("No textures assigned."), DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!externalTextures.empty()) {
|
||||||
|
// Check External Textures:
|
||||||
|
auto modelTexturesURLs = model->getTextures();
|
||||||
|
_externalTextureCount = externalTextures.length();
|
||||||
|
|
||||||
|
auto checkTextureLoadingComplete = [this]() mutable {
|
||||||
|
if (_checkedTextureCount == _externalTextureCount) {
|
||||||
|
if (_missingTextureCount > 0) {
|
||||||
|
_errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
if (_unsupportedTextureCount > 0) {
|
||||||
|
_errors.push_back({ tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount),
|
||||||
|
DEFAULT_DOCS_URL });
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QVariantList AvatarDoctor::getErrors() const {
|
QVariantList AvatarDoctor::getErrors() const {
|
||||||
QVariantList result;
|
QVariantList result;
|
||||||
for (const auto& error : _errors) {
|
for (const auto& error : _errors) {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
|
#include "GeometryCache.h"
|
||||||
|
|
||||||
struct AvatarDiagnosticResult {
|
struct AvatarDiagnosticResult {
|
||||||
QString message;
|
QString message;
|
||||||
|
@ -27,7 +28,7 @@ Q_DECLARE_METATYPE(QVector<AvatarDiagnosticResult>)
|
||||||
class AvatarDoctor : public QObject {
|
class AvatarDoctor : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
AvatarDoctor(QUrl avatarFSTFileUrl);
|
AvatarDoctor(const QUrl& avatarFSTFileUrl);
|
||||||
|
|
||||||
Q_INVOKABLE void startDiagnosing();
|
Q_INVOKABLE void startDiagnosing();
|
||||||
|
|
||||||
|
@ -37,6 +38,8 @@ signals:
|
||||||
void complete(QVariantList errors);
|
void complete(QVariantList errors);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void diagnoseTextures();
|
||||||
|
|
||||||
QUrl _avatarFSTFileUrl;
|
QUrl _avatarFSTFileUrl;
|
||||||
QVector<AvatarDiagnosticResult> _errors;
|
QVector<AvatarDiagnosticResult> _errors;
|
||||||
|
|
||||||
|
@ -45,6 +48,11 @@ private:
|
||||||
int _missingTextureCount = 0;
|
int _missingTextureCount = 0;
|
||||||
int _unsupportedTextureCount = 0;
|
int _unsupportedTextureCount = 0;
|
||||||
|
|
||||||
|
int _materialMappingCount = 0;
|
||||||
|
int _materialMappingLoadedCount = 0;
|
||||||
|
|
||||||
|
GeometryResource::Pointer _model;
|
||||||
|
|
||||||
bool _isDiagnosing = false;
|
bool _isDiagnosing = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2179,3 +2179,52 @@ void Rig::initFlow(bool isActive) {
|
||||||
_networkFlow.cleanUp();
|
_networkFlow.cleanUp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float Rig::getUnscaledEyeHeight() const {
|
||||||
|
// Normally the model offset transform will contain the avatar scale factor, we explicitly remove it here.
|
||||||
|
AnimPose modelOffsetWithoutAvatarScale(glm::vec3(1.0f), getModelOffsetPose().rot(), getModelOffsetPose().trans());
|
||||||
|
AnimPose geomToRigWithoutAvatarScale = modelOffsetWithoutAvatarScale * getGeometryOffsetPose();
|
||||||
|
|
||||||
|
// This factor can be used to scale distances in the geometry frame into the unscaled rig frame.
|
||||||
|
// Typically it will be the unit conversion from cm to m.
|
||||||
|
float scaleFactor = geomToRigWithoutAvatarScale.scale().x; // in practice this always a uniform scale factor.
|
||||||
|
|
||||||
|
int headTopJoint = indexOfJoint("HeadTop_End");
|
||||||
|
int headJoint = indexOfJoint("Head");
|
||||||
|
int eyeJoint = indexOfJoint("LeftEye") != -1 ? indexOfJoint("LeftEye") : indexOfJoint("RightEye");
|
||||||
|
int toeJoint = indexOfJoint("LeftToeBase") != -1 ? indexOfJoint("LeftToeBase") : indexOfJoint("RightToeBase");
|
||||||
|
|
||||||
|
// Makes assumption that the y = 0 plane in geometry is the ground plane.
|
||||||
|
// We also make that assumption in Rig::computeAvatarBoundingCapsule()
|
||||||
|
const float GROUND_Y = 0.0f;
|
||||||
|
|
||||||
|
// Values from the skeleton are in the geometry coordinate frame.
|
||||||
|
auto skeleton = getAnimSkeleton();
|
||||||
|
if (eyeJoint >= 0 && toeJoint >= 0) {
|
||||||
|
// Measure from eyes to toes.
|
||||||
|
float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - skeleton->getAbsoluteDefaultPose(toeJoint).trans().y;
|
||||||
|
return scaleFactor * eyeHeight;
|
||||||
|
} else if (eyeJoint >= 0) {
|
||||||
|
// Measure Eye joint to y = 0 plane.
|
||||||
|
float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - GROUND_Y;
|
||||||
|
return scaleFactor * eyeHeight;
|
||||||
|
} else if (headTopJoint >= 0 && toeJoint >= 0) {
|
||||||
|
// Measure from ToeBase joint to HeadTop_End joint, then remove forehead distance.
|
||||||
|
const float ratio = DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD / DEFAULT_AVATAR_HEIGHT;
|
||||||
|
float height = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - skeleton->getAbsoluteDefaultPose(toeJoint).trans().y;
|
||||||
|
return scaleFactor * (height - height * ratio);
|
||||||
|
} else if (headTopJoint >= 0) {
|
||||||
|
// Measure from HeadTop_End joint to the ground, then remove forehead distance.
|
||||||
|
const float ratio = DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD / DEFAULT_AVATAR_HEIGHT;
|
||||||
|
float headHeight = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - GROUND_Y;
|
||||||
|
return scaleFactor * (headHeight - headHeight * ratio);
|
||||||
|
} else if (headJoint >= 0) {
|
||||||
|
// Measure Head joint to the ground, then add in distance from neck to eye.
|
||||||
|
const float DEFAULT_AVATAR_NECK_TO_EYE = DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD;
|
||||||
|
const float ratio = DEFAULT_AVATAR_NECK_TO_EYE / DEFAULT_AVATAR_NECK_HEIGHT;
|
||||||
|
float neckHeight = skeleton->getAbsoluteDefaultPose(headJoint).trans().y - GROUND_Y;
|
||||||
|
return scaleFactor * (neckHeight + neckHeight * ratio);
|
||||||
|
} else {
|
||||||
|
return DEFAULT_AVATAR_EYE_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
#include "SimpleMovingAverage.h"
|
#include "SimpleMovingAverage.h"
|
||||||
#include "AnimUtil.h"
|
#include "AnimUtil.h"
|
||||||
#include "Flow.h"
|
#include "Flow.h"
|
||||||
|
#include "AvatarConstants.h"
|
||||||
|
|
||||||
class Rig;
|
class Rig;
|
||||||
class AnimInverseKinematics;
|
class AnimInverseKinematics;
|
||||||
|
@ -237,6 +238,8 @@ public:
|
||||||
void initFlow(bool isActive);
|
void initFlow(bool isActive);
|
||||||
Flow& getFlow() { return _internalFlow; }
|
Flow& getFlow() { return _internalFlow; }
|
||||||
|
|
||||||
|
float getUnscaledEyeHeight() const;
|
||||||
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void onLoadComplete();
|
void onLoadComplete();
|
||||||
|
|
|
@ -2040,54 +2040,7 @@ float Avatar::getUnscaledEyeHeightFromSkeleton() const {
|
||||||
// TODO: if performance becomes a concern we can cache this value rather then computing it everytime.
|
// TODO: if performance becomes a concern we can cache this value rather then computing it everytime.
|
||||||
|
|
||||||
if (_skeletonModel) {
|
if (_skeletonModel) {
|
||||||
auto& rig = _skeletonModel->getRig();
|
return _skeletonModel->getRig().getUnscaledEyeHeight();
|
||||||
|
|
||||||
// Normally the model offset transform will contain the avatar scale factor, we explicitly remove it here.
|
|
||||||
AnimPose modelOffsetWithoutAvatarScale(glm::vec3(1.0f), rig.getModelOffsetPose().rot(), rig.getModelOffsetPose().trans());
|
|
||||||
AnimPose geomToRigWithoutAvatarScale = modelOffsetWithoutAvatarScale * rig.getGeometryOffsetPose();
|
|
||||||
|
|
||||||
// This factor can be used to scale distances in the geometry frame into the unscaled rig frame.
|
|
||||||
// Typically it will be the unit conversion from cm to m.
|
|
||||||
float scaleFactor = geomToRigWithoutAvatarScale.scale().x; // in practice this always a uniform scale factor.
|
|
||||||
|
|
||||||
int headTopJoint = rig.indexOfJoint("HeadTop_End");
|
|
||||||
int headJoint = rig.indexOfJoint("Head");
|
|
||||||
int eyeJoint = rig.indexOfJoint("LeftEye") != -1 ? rig.indexOfJoint("LeftEye") : rig.indexOfJoint("RightEye");
|
|
||||||
int toeJoint = rig.indexOfJoint("LeftToeBase") != -1 ? rig.indexOfJoint("LeftToeBase") : rig.indexOfJoint("RightToeBase");
|
|
||||||
|
|
||||||
// Makes assumption that the y = 0 plane in geometry is the ground plane.
|
|
||||||
// We also make that assumption in Rig::computeAvatarBoundingCapsule()
|
|
||||||
const float GROUND_Y = 0.0f;
|
|
||||||
|
|
||||||
// Values from the skeleton are in the geometry coordinate frame.
|
|
||||||
auto skeleton = rig.getAnimSkeleton();
|
|
||||||
if (eyeJoint >= 0 && toeJoint >= 0) {
|
|
||||||
// Measure from eyes to toes.
|
|
||||||
float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - skeleton->getAbsoluteDefaultPose(toeJoint).trans().y;
|
|
||||||
return scaleFactor * eyeHeight;
|
|
||||||
} else if (eyeJoint >= 0) {
|
|
||||||
// Measure Eye joint to y = 0 plane.
|
|
||||||
float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - GROUND_Y;
|
|
||||||
return scaleFactor * eyeHeight;
|
|
||||||
} else if (headTopJoint >= 0 && toeJoint >= 0) {
|
|
||||||
// Measure from ToeBase joint to HeadTop_End joint, then remove forehead distance.
|
|
||||||
const float ratio = DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD / DEFAULT_AVATAR_HEIGHT;
|
|
||||||
float height = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - skeleton->getAbsoluteDefaultPose(toeJoint).trans().y;
|
|
||||||
return scaleFactor * (height - height * ratio);
|
|
||||||
} else if (headTopJoint >= 0) {
|
|
||||||
// Measure from HeadTop_End joint to the ground, then remove forehead distance.
|
|
||||||
const float ratio = DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD / DEFAULT_AVATAR_HEIGHT;
|
|
||||||
float headHeight = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - GROUND_Y;
|
|
||||||
return scaleFactor * (headHeight - headHeight * ratio);
|
|
||||||
} else if (headJoint >= 0) {
|
|
||||||
// Measure Head joint to the ground, then add in distance from neck to eye.
|
|
||||||
const float DEFAULT_AVATAR_NECK_TO_EYE = DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD;
|
|
||||||
const float ratio = DEFAULT_AVATAR_NECK_TO_EYE / DEFAULT_AVATAR_NECK_HEIGHT;
|
|
||||||
float neckHeight = skeleton->getAbsoluteDefaultPose(headJoint).trans().y - GROUND_Y;
|
|
||||||
return scaleFactor * (neckHeight + neckHeight * ratio);
|
|
||||||
} else {
|
|
||||||
return DEFAULT_AVATAR_EYE_HEIGHT;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return DEFAULT_AVATAR_EYE_HEIGHT;
|
return DEFAULT_AVATAR_EYE_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue