diff --git a/interface/src/ModelUploader.cpp b/interface/src/ModelUploader.cpp index 048e13bdf2..799301b541 100644 --- a/interface/src/ModelUploader.cpp +++ b/interface/src/ModelUploader.cpp @@ -9,21 +9,29 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include +#include +#include #include #include +#include #include +#include #include +#include +#include #include #include +#include #include +#include #include +#include #include #include -#include - #include "Application.h" #include "ModelUploader.h" @@ -32,6 +40,10 @@ static const QString NAME_FIELD = "name"; static const QString FILENAME_FIELD = "filename"; static const QString TEXDIR_FIELD = "texdir"; static const QString LOD_FIELD = "lod"; +static const QString JOINT_INDEX_FIELD = "jointIndex"; +static const QString SCALE_FIELD = "scale"; +static const QString JOINT_FIELD = "joint"; +static const QString FREE_JOINT_FIELD = "freeJoint"; static const QString S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com"; static const QString DATA_SERVER_URL = "https://data-web.highfidelity.io"; @@ -40,6 +52,7 @@ static const QString MODEL_URL = "/api/v1/models"; static const QString SETTING_NAME = "LastModelUploadLocation"; static const int MAX_SIZE = 10 * 1024 * 1024; // 10 MB +static const int MAX_TEXTURE_SIZE = 1024; static const int TIMEOUT = 1000; static const int MAX_CHECK = 30; @@ -76,7 +89,8 @@ bool ModelUploader::zip() { } - QString filename = QFileDialog::getOpenFileName(NULL, "Select your .fst file ...", lastLocation, "*.fst"); + QString filename = QFileDialog::getOpenFileName(NULL, "Select your model file ...", + lastLocation, "Model files (*.fst *.fbx)"); if (filename == "") { // If the user canceled we return. Application::getInstance()->unlockSettings(); @@ -85,89 +99,160 @@ bool ModelUploader::zip() { settings->setValue(SETTING_NAME, filename); Application::getInstance()->unlockSettings(); - bool _nameIsPresent = false; - QString texDir; + // First we check the FST file (if any) + QFile* fst; + QVariantHash mapping; + QString basePath; QString fbxFile; + if (filename.toLower().endsWith(".fst")) { + fst = new QFile(filename, this); + if (!fst->open(QFile::ReadOnly | QFile::Text)) { + QMessageBox::warning(NULL, + QString("ModelUploader::zip()"), + QString("Could not open FST file."), + QMessageBox::Ok); + qDebug() << "[Warning] " << QString("Could not open FST file."); + return false; + } + qDebug() << "Reading FST file : " << QFileInfo(*fst).filePath(); + mapping = readMapping(fst->readAll()); + basePath = QFileInfo(*fst).path(); + fbxFile = basePath + "/" + mapping.value(FILENAME_FIELD).toString(); + QFileInfo fbxInfo(fbxFile); + if (!fbxInfo.exists() || !fbxInfo.isFile()) { // Check existence + QMessageBox::warning(NULL, + QString("ModelUploader::zip()"), + QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()), + QMessageBox::Ok); + qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()); + return false; + } + } else { + fst = new QTemporaryFile(this); + fst->open(QFile::WriteOnly); + fbxFile = filename; + basePath = QFileInfo(filename).path(); + mapping.insert(FILENAME_FIELD, QFileInfo(filename).fileName()); + } + // open the fbx file + QFile fbx(fbxFile); + if (!fbx.open(QIODevice::ReadOnly)) { + return false; + } + QByteArray fbxContents = fbx.readAll(); + FBXGeometry geometry = readFBX(fbxContents, QVariantHash()); - // First we check the FST file - QFile fst(filename); - if (!fst.open(QFile::ReadOnly | QFile::Text)) { + // make sure we have some basic mappings + if (!mapping.contains(NAME_FIELD)) { + mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); + } + if (!mapping.contains(TEXDIR_FIELD)) { + mapping.insert(TEXDIR_FIELD, "."); + } + + // mixamo/autodesk defaults + if (!mapping.contains(SCALE_FIELD)) { + mapping.insert(SCALE_FIELD, 10.0); + } + QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); + if (!joints.contains("jointEyeLeft")) { + joints.insert("jointEyeLeft", "LeftEye"); + } + if (!joints.contains("jointEyeRight")) { + joints.insert("jointEyeRight", "RightEye"); + } + if (!joints.contains("jointNeck")) { + joints.insert("jointNeck", "Neck"); + } + if (!joints.contains("jointRoot")) { + joints.insert("jointRoot", "Hips"); + } + if (!joints.contains("jointLean")) { + joints.insert("jointLean", "Spine"); + } + if (!joints.contains("jointHead")) { + joints.insert("jointHead", geometry.applicationName == "mixamo.com" ? "HeadTop_End" : "HeadEnd"); + } + if (!joints.contains("jointLeftHand")) { + joints.insert("jointLeftHand", "LeftHand"); + } + if (!joints.contains("jointRightHand")) { + joints.insert("jointRightHand", "RightHand"); + } + mapping.insert(JOINT_FIELD, joints); + if (!mapping.contains(FREE_JOINT_FIELD)) { + mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); + } + + // open the dialog to configure the rest + ModelPropertiesDialog properties(_isHead, mapping, basePath, geometry); + if (properties.exec() == QDialog::Rejected) { + return false; + } + mapping = properties.getMapping(); + + QByteArray nameField = mapping.value(NAME_FIELD).toByteArray(); + if (!nameField.isEmpty()) { + QHttpPart textPart; + textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"model_name\""); + textPart.setBody(nameField); + _dataMultiPart->append(textPart); + _url = S3_URL + ((_isHead)? "/models/heads/" : "/models/skeletons/") + nameField + ".fst"; + } else { QMessageBox::warning(NULL, QString("ModelUploader::zip()"), - QString("Could not open FST file."), + QString("Model name is missing in the .fst file."), QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Could not open FST file."); - return false; - } - qDebug() << "Reading FST file : " << QFileInfo(fst).filePath(); - - // Compress and copy the fst - if (!addPart(QFileInfo(fst).filePath(), QString("fst"))) { + qDebug() << "[Warning] " << QString("Model name is missing in the .fst file."); return false; } - // Let's read through the FST file - QTextStream stream(&fst); - QList line; - while (!stream.atEnd()) { - line = stream.readLine().split(QRegExp("[ =]"), QString::SkipEmptyParts); - if (line.isEmpty()) { - continue; - } - - // according to what is read, we modify the command - if (line[0] == NAME_FIELD) { - QHttpPart textPart; - textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;" - " name=\"model_name\""); - textPart.setBody(line[1].toUtf8()); - _dataMultiPart->append(textPart); - _url = S3_URL + ((_isHead)? "/models/heads/" : "/models/skeletons/") + line[1].toUtf8() + ".fst"; - _nameIsPresent = true; - } else if (line[0] == FILENAME_FIELD) { - fbxFile = QFileInfo(fst).path() + "/" + line[1]; - QFileInfo fbxInfo(fbxFile); - if (!fbxInfo.exists() || !fbxInfo.isFile()) { // Check existence - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()); - return false; - } - // Compress and copy - if (!addPart(fbxInfo.filePath(), "fbx")) { - return false; - } - } else if (line[0] == TEXDIR_FIELD) { // Check existence - texDir = QFileInfo(fst).path() + "/" + line[1]; - QFileInfo texInfo(texDir); - if (!texInfo.exists() || !texInfo.isDir()) { - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("Texture directory could not be found."), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Texture directory could not be found."); - return false; - } - } else if (line[0] == LOD_FIELD) { - QFileInfo lod(QFileInfo(fst).path() + "/" + line[1]); - if (!lod.exists() || !lod.isFile()) { // Check existence - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("LOD file %1 could not be found.").arg(lod.fileName()), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(lod.fileName()); - } - // Compress and copy - if (!addPart(lod.filePath(), QString("lod%1").arg(++_lodCount))) { - return false; - } + QByteArray texdirField = mapping.value(TEXDIR_FIELD).toByteArray(); + QString texDir; + if (!texdirField.isEmpty()) { + texDir = basePath + "/" + texdirField; + QFileInfo texInfo(texDir); + if (!texInfo.exists() || !texInfo.isDir()) { + QMessageBox::warning(NULL, + QString("ModelUploader::zip()"), + QString("Texture directory could not be found."), + QMessageBox::Ok); + qDebug() << "[Warning] " << QString("Texture directory could not be found."); + return false; } } - if (!addTextures(texDir, fbxFile)) { + QVariantHash lodField = mapping.value(LOD_FIELD).toHash(); + for (QVariantHash::const_iterator it = lodField.constBegin(); it != lodField.constEnd(); it++) { + QFileInfo lod(basePath + "/" + it.key()); + if (!lod.exists() || !lod.isFile()) { // Check existence + QMessageBox::warning(NULL, + QString("ModelUploader::zip()"), + QString("LOD file %1 could not be found.").arg(lod.fileName()), + QMessageBox::Ok); + qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(lod.fileName()); + } + // Compress and copy + if (!addPart(lod.filePath(), QString("lod%1").arg(++_lodCount))) { + return false; + } + } + + // Write out, compress and copy the fst + if (!addPart(*fst, writeMapping(mapping), QString("fst"))) { + return false; + } + + // Compress and copy the fbx + if (!addPart(fbx, fbxContents, "fbx")) { + return false; + } + + if (!addTextures(texDir, geometry)) { return false; } @@ -181,15 +266,6 @@ bool ModelUploader::zip() { } _dataMultiPart->append(textPart); - if (!_nameIsPresent) { - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("Model name is missing in the .fst file."), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Model name is missing in the .fst file."); - return false; - } - _readyToSend = true; return true; } @@ -350,27 +426,18 @@ void ModelUploader::processCheck() { delete reply; } -bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) { - QFile fbx(fbxFile); - if (!fbx.open(QIODevice::ReadOnly)) { - return false; - } - - QByteArray buffer = fbx.readAll(); - QVariantHash variantHash = readMapping(buffer); - FBXGeometry geometry = readFBX(buffer, variantHash); - +bool ModelUploader::addTextures(const QString& texdir, const FBXGeometry& geometry) { foreach (FBXMesh mesh, geometry.meshes) { foreach (FBXMeshPart part, mesh.parts) { - if (!part.diffuseTexture.filename.isEmpty()) { + if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty()) { if (!addPart(texdir + "/" + part.diffuseTexture.filename, - QString("texture%1").arg(++_texturesCount))) { + QString("texture%1").arg(++_texturesCount), true)) { return false; } } - if (!part.normalTexture.filename.isEmpty()) { + if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty()) { if (!addPart(texdir + "/" + part.normalTexture.filename, - QString("texture%1").arg(++_texturesCount))) { + QString("texture%1").arg(++_texturesCount), true)) { return false; } } @@ -380,7 +447,7 @@ bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) { return true; } -bool ModelUploader::addPart(const QString &path, const QString& name) { +bool ModelUploader::addPart(const QString &path, const QString& name, bool isTexture) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(NULL, @@ -390,7 +457,29 @@ bool ModelUploader::addPart(const QString &path, const QString& name) { qDebug() << "[Warning] " << QString("Could not open %1").arg(path); return false; } - QByteArray buffer = qCompress(file.readAll()); + return addPart(file, file.readAll(), name, isTexture); +} + +bool ModelUploader::addPart(const QFile& file, const QByteArray& contents, const QString& name, bool isTexture) { + QFileInfo fileInfo(file); + QByteArray recodedContents = contents; + if (isTexture) { + QString extension = fileInfo.suffix().toLower(); + bool isJpeg = (extension == "jpg"); + bool mustRecode = !(isJpeg || extension == "png"); + QImage image = QImage::fromData(contents); + if (image.width() > MAX_TEXTURE_SIZE || image.height() > MAX_TEXTURE_SIZE) { + image = image.scaled(MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, Qt::KeepAspectRatio); + mustRecode = true; + } + if (mustRecode) { + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + image.save(&buffer, isJpeg ? "JPG" : "PNG"); + recodedContents = buffer.data(); + } + } + QByteArray buffer = qCompress(recodedContents); // Qt's qCompress() default compression level (-1) is the standard zLib compression. // Here remove Qt's custom header that prevent the data server from uncompressing the files with zLib. @@ -406,7 +495,7 @@ bool ModelUploader::addPart(const QString &path, const QString& name) { qDebug() << "File " << QFileInfo(file).fileName() << " added to model."; - _totalSize += file.size(); + _totalSize += recodedContents.size(); if (_totalSize > MAX_SIZE) { QMessageBox::warning(NULL, QString("ModelUploader::zip()"), @@ -420,8 +509,165 @@ bool ModelUploader::addPart(const QString &path, const QString& name) { return true; } +ModelPropertiesDialog::ModelPropertiesDialog(bool isHead, const QVariantHash& originalMapping, + const QString& basePath, const FBXGeometry& geometry) : + _isHead(isHead), + _originalMapping(originalMapping), + _basePath(basePath), + _geometry(geometry) { + + setWindowTitle("Set Model Properties"); + + QFormLayout* form = new QFormLayout(); + setLayout(form); + + form->addRow("Name:", _name = new QLineEdit()); + + form->addRow("Texture Directory:", _textureDirectory = new QPushButton()); + connect(_textureDirectory, SIGNAL(clicked(bool)), SLOT(chooseTextureDirectory())); + + form->addRow("Scale:", _scale = new QDoubleSpinBox()); + _scale->setMaximum(FLT_MAX); + _scale->setSingleStep(0.01); + + form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); + form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); + form->addRow("Neck Joint:", _neckJoint = createJointBox()); + if (!isHead) { + form->addRow("Root Joint:", _rootJoint = createJointBox()); + form->addRow("Lean Joint:", _leanJoint = createJointBox()); + form->addRow("Head Joint:", _headJoint = createJointBox()); + form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); + form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); + + form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); + QPushButton* newFreeJoint = new QPushButton("New Free Joint"); + _freeJoints->addWidget(newFreeJoint); + connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); + } + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel | QDialogButtonBox::Reset); + connect(buttons, SIGNAL(accepted()), SLOT(accept())); + connect(buttons, SIGNAL(rejected()), SLOT(reject())); + connect(buttons->button(QDialogButtonBox::Reset), SIGNAL(clicked(bool)), SLOT(reset())); + + form->addRow(buttons); + + // reset to initialize the fields + reset(); +} +QVariantHash ModelPropertiesDialog::getMapping() const { + QVariantHash mapping = _originalMapping; + mapping.insert(NAME_FIELD, _name->text()); + mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); + mapping.insert(SCALE_FIELD, QString::number(_scale->value())); + + // update the joint indices + QVariantHash jointIndices; + for (int i = 0; i < _geometry.joints.size(); i++) { + jointIndices.insert(_geometry.joints.at(i).name, QString::number(i)); + } + mapping.insert(JOINT_INDEX_FIELD, jointIndices); + + QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); + insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); + insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); + insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); + if (!_isHead) { + insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); + insertJointMapping(joints, "jointLean", _leanJoint->currentText()); + insertJointMapping(joints, "jointHead", _headJoint->currentText()); + insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); + insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); + + mapping.remove(FREE_JOINT_FIELD); + for (int i = 0; i < _freeJoints->count() - 1; i++) { + QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); + mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); + } + } + mapping.insert(JOINT_FIELD, joints); + + return mapping; +} +static void setJointText(QComboBox* box, const QString& text) { + box->setCurrentIndex(qMax(box->findText(text), 0)); +} +void ModelPropertiesDialog::reset() { + _name->setText(_originalMapping.value(NAME_FIELD).toString()); + _textureDirectory->setText(_originalMapping.value(TEXDIR_FIELD).toString()); + _scale->setValue(_originalMapping.value(SCALE_FIELD, 1.0).toDouble()); + + QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); + setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString()); + setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); + setJointText(_neckJoint, jointHash.value("jointNeck").toString()); + if (!_isHead) { + setJointText(_rootJoint, jointHash.value("jointRoot").toString()); + setJointText(_leanJoint, jointHash.value("jointLean").toString()); + setJointText(_headJoint, jointHash.value("jointHead").toString()); + setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString()); + setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString()); + + while (_freeJoints->count() > 1) { + delete _freeJoints->itemAt(0)->widget(); + } + foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { + QString jointName = joint.toString(); + if (_geometry.jointIndices.contains(jointName)) { + createNewFreeJoint(jointName); + } + } + } +} +void ModelPropertiesDialog::chooseTextureDirectory() { + QString directory = QFileDialog::getExistingDirectory(this, "Choose Texture Directory", + _basePath + "/" + _textureDirectory->text()); + if (directory.isEmpty()) { + return; + } + if (!directory.startsWith(_basePath)) { + QMessageBox::warning(NULL, "Invalid texture directory", "Texture directory must be child of base path."); + return; + } + _textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1)); +} + +void ModelPropertiesDialog::createNewFreeJoint(const QString& joint) { + QWidget* freeJoint = new QWidget(); + QHBoxLayout* freeJointLayout = new QHBoxLayout(); + freeJointLayout->setContentsMargins(QMargins()); + freeJoint->setLayout(freeJointLayout); + QComboBox* jointBox = createJointBox(false); + jointBox->setCurrentText(joint); + freeJointLayout->addWidget(jointBox, 1); + QPushButton* deleteJoint = new QPushButton("Delete"); + freeJointLayout->addWidget(deleteJoint); + freeJoint->connect(deleteJoint, SIGNAL(clicked(bool)), SLOT(deleteLater())); + _freeJoints->insertWidget(_freeJoints->count() - 1, freeJoint); +} + +QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { + QComboBox* box = new QComboBox(); + if (withNone) { + box->addItem("(none)"); + } + foreach (const FBXJoint& joint, _geometry.joints) { + box->addItem(joint.name); + } + return box; +} + +void ModelPropertiesDialog::insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const { + if (_geometry.jointIndices.contains(name)) { + joints.insert(joint, name); + } else { + joints.remove(joint); + } +} diff --git a/interface/src/ModelUploader.h b/interface/src/ModelUploader.h index 54702d6420..11594b3d95 100644 --- a/interface/src/ModelUploader.h +++ b/interface/src/ModelUploader.h @@ -12,12 +12,19 @@ #ifndef hifi_ModelUploader_h #define hifi_ModelUploader_h +#include #include -class QDialog; +#include + +class QComboBox; +class QDoubleSpinBox; class QFileInfo; class QHttpMultiPart; +class QLineEdit; class QProgressBar; +class QPushButton; +class QVBoxLayout; class ModelUploader : public QObject { Q_OBJECT @@ -56,8 +63,46 @@ private: bool zip(); - bool addTextures(const QString& texdir, const QString fbxFile); - bool addPart(const QString& path, const QString& name); + bool addTextures(const QString& texdir, const FBXGeometry& geometry); + bool addPart(const QString& path, const QString& name, bool isTexture = false); + bool addPart(const QFile& file, const QByteArray& contents, const QString& name, bool isTexture = false); +}; + +/// A dialog that allows customization of various model properties. +class ModelPropertiesDialog : public QDialog { + Q_OBJECT + +public: + ModelPropertiesDialog(bool isHead, const QVariantHash& originalMapping, + const QString& basePath, const FBXGeometry& geometry); + + QVariantHash getMapping() const; + +private slots: + void reset(); + void chooseTextureDirectory(); + void createNewFreeJoint(const QString& joint = QString()); + +private: + QComboBox* createJointBox(bool withNone = true) const; + void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; + + bool _isHead; + QVariantHash _originalMapping; + QString _basePath; + FBXGeometry _geometry; + QLineEdit* _name; + QPushButton* _textureDirectory; + QDoubleSpinBox* _scale; + QComboBox* _leftEyeJoint; + QComboBox* _rightEyeJoint; + QComboBox* _neckJoint; + QComboBox* _rootJoint; + QComboBox* _leanJoint; + QComboBox* _headJoint; + QComboBox* _leftHandJoint; + QComboBox* _rightHandJoint; + QVBoxLayout* _freeJoints; }; #endif // hifi_ModelUploader_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 23cee8b452..a85562fccf 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -517,7 +517,7 @@ void MyAvatar::loadData(QSettings* settings) { setScale(_scale); Application::getInstance()->getCamera()->setScale(_scale); - setFaceModelURL(settings->value("faceModelURL").toUrl()); + setFaceModelURL(settings->value("faceModelURL", DEFAULT_HEAD_MODEL_URL).toUrl()); setSkeletonModelURL(settings->value("skeletonModelURL").toUrl()); setDisplayName(settings->value("displayName").toString()); diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp index 3a410ac5e2..6e93fc77af 100644 --- a/interface/src/renderer/GeometryCache.cpp +++ b/interface/src/renderer/GeometryCache.cpp @@ -305,7 +305,6 @@ void GeometryCache::setBlendedVertices(const QPointer& model, const QWeak QSharedPointer GeometryCache::createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra) { - QSharedPointer geometry(new NetworkGeometry(url, fallback.staticCast(), delayLoad), &Resource::allReferencesCleared); geometry->setLODParent(geometry); @@ -320,6 +319,20 @@ NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer(), -1 }; + _geometry.joints.append(joint); + _geometry.leftEyeJointIndex = -1; + _geometry.rightEyeJointIndex = -1; + _geometry.neckJointIndex = -1; + _geometry.rootJointIndex = -1; + _geometry.leanJointIndex = -1; + _geometry.headJointIndex = -1; + _geometry.leftHandJointIndex = -1; + _geometry.rightHandJointIndex = -1; + } } bool NetworkGeometry::isLoadedWithTextures() const { diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 6bc5afd4fe..238e596c5f 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -44,7 +44,8 @@ Model::Model(QObject* parent) : _boundingShape(), _boundingShapeLocalOffset(0.f), _lodDistance(0.0f), - _pupilDilation(0.0f) { + _pupilDilation(0.0f), + _url("http://invalid.com") { // we may have been created in the network thread, but we live in the main thread moveToThread(Application::getInstance()->thread()); } diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 4105b229b3..14589d1464 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -62,7 +62,7 @@ public: bool isActive() const { return _geometry && _geometry->isLoaded(); } - bool isRenderable() const { return !_meshStates.isEmpty(); } + bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && _geometry->getMeshes().isEmpty()); } bool isLoadedWithTextures() const { return _geometry && _geometry->isLoadedWithTextures(); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 2e716296ff..b57d5406d5 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -46,7 +46,8 @@ AvatarData::AvatarData() : _isChatCirclingEnabled(false), _hasNewJointRotations(true), _headData(NULL), - _handData(NULL), + _handData(NULL), + _faceModelURL("http://invalid.com"), _displayNameBoundingRect(), _displayNameTargetAlpha(0.0f), _displayNameAlpha(0.0f), @@ -640,7 +641,7 @@ bool AvatarData::hasBillboardChangedAfterParsing(const QByteArray& packet) { } void AvatarData::setFaceModelURL(const QUrl& faceModelURL) { - _faceModelURL = faceModelURL.isEmpty() ? DEFAULT_HEAD_MODEL_URL : faceModelURL; + _faceModelURL = faceModelURL; qDebug() << "Changing face model for avatar to" << _faceModelURL.toString(); } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 7692d81eb9..9389d0abf8 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -584,13 +584,16 @@ public: }; glm::mat4 getGlobalTransform(const QMultiHash& parentMap, - const QHash& models, QString nodeID) { + const QHash& models, QString nodeID, bool mixamoHack) { glm::mat4 globalTransform; while (!nodeID.isNull()) { const FBXModel& model = models.value(nodeID); globalTransform = glm::translate(model.translation) * model.preTransform * glm::mat4_cast(model.preRotation * model.rotation * model.postRotation) * model.postTransform * globalTransform; - + if (mixamoHack) { + // there's something weird about the models from Mixamo Fuse; they don't skin right with the full transform + return globalTransform; + } QList parentIDs = parentMap.values(nodeID); nodeID = QString(); foreach (const QString& parentID, parentIDs) { @@ -1006,9 +1009,25 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } } QMultiHash blendshapeChannelIndices; - + + FBXGeometry geometry; foreach (const FBXNode& child, node.children) { - if (child.name == "Objects") { + if (child.name == "FBXHeaderExtension") { + foreach (const FBXNode& object, child.children) { + if (object.name == "SceneInfo") { + foreach (const FBXNode& subobject, object.children) { + if (subobject.name == "Properties70") { + foreach (const FBXNode& subsubobject, subobject.children) { + if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && + subsubobject.properties.at(0) == "Original|ApplicationName") { + geometry.applicationName = subsubobject.properties.at(4).toString(); + } + } + } + } + } + } + } else if (child.name == "Objects") { foreach (const FBXNode& object, child.children) { if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { @@ -1317,7 +1336,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) } // get offset transform from mapping - FBXGeometry geometry; float offsetScale = mapping.value("scale", 1.0f).toFloat(); glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(), mapping.value("ry").toFloat(), mapping.value("rz").toFloat()))); @@ -1466,7 +1484,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) // accumulate local transforms QString modelID = models.contains(it.key()) ? it.key() : parentMap.value(it.key()); - glm::mat4 modelTransform = getGlobalTransform(parentMap, models, modelID); + glm::mat4 modelTransform = getGlobalTransform(parentMap, models, modelID, geometry.applicationName == "mixamo.com"); // compute the mesh extents from the transformed vertices foreach (const glm::vec3& vertex, extracted.mesh.vertices) { @@ -1803,6 +1821,33 @@ QVariantHash readMapping(const QByteArray& data) { return parseMapping(&buffer); } +QByteArray writeMapping(const QVariantHash& mapping) { + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + for (QVariantHash::const_iterator first = mapping.constBegin(); first != mapping.constEnd(); first++) { + QByteArray key = first.key().toUtf8() + " = "; + QVariantHash hashValue = first.value().toHash(); + if (hashValue.isEmpty()) { + buffer.write(key + first.value().toByteArray() + "\n"); + continue; + } + for (QVariantHash::const_iterator second = hashValue.constBegin(); second != hashValue.constEnd(); second++) { + QByteArray extendedKey = key + second.key().toUtf8(); + QVariantList listValue = second.value().toList(); + if (listValue.isEmpty()) { + buffer.write(extendedKey + " = " + second.value().toByteArray() + "\n"); + continue; + } + buffer.write(extendedKey); + for (QVariantList::const_iterator third = listValue.constBegin(); third != listValue.constEnd(); third++) { + buffer.write(" = " + third->toByteArray()); + } + buffer.write("\n"); + } + } + return buffer.data(); +} + FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index e437961385..ea8b8f517d 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -174,6 +174,8 @@ public: class FBXGeometry { public: + QString applicationName; ///< the name of the application that generated the model + QVector joints; QHash jointIndices; ///< 1-based, so as to more easily detect missing indices @@ -218,6 +220,9 @@ Q_DECLARE_METATYPE(FBXGeometry) /// Reads an FST mapping from the supplied data. QVariantHash readMapping(const QByteArray& data); +/// Writes an FST mapping to a byte array. +QByteArray writeMapping(const QVariantHash& mapping); + /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping); diff --git a/libraries/shared/src/ResourceCache.cpp b/libraries/shared/src/ResourceCache.cpp index 04b6265513..2f26e344fd 100644 --- a/libraries/shared/src/ResourceCache.cpp +++ b/libraries/shared/src/ResourceCache.cpp @@ -31,7 +31,7 @@ ResourceCache::~ResourceCache() { } QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& fallback, bool delayLoad, void* extra) { - if (!url.isValid() && fallback.isValid()) { + if (!url.isValid() && !url.isEmpty() && fallback.isValid()) { return getResource(fallback, QUrl(), delayLoad); } QSharedPointer resource = _resources.value(url); @@ -114,7 +114,11 @@ Resource::Resource(const QUrl& url, bool delayLoad) : _reply(NULL), _attempts(0) { - if (!(url.isValid() && ResourceCache::getNetworkAccessManager())) { + if (url.isEmpty()) { + _startedLoading = _loaded = true; + return; + + } else if (!(url.isValid() && ResourceCache::getNetworkAccessManager())) { _startedLoading = _failedToLoad = true; return; }