mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-07-26 08:34:02 +02:00
Merge pull request #2767 from ey6es/master
Allow uploading FBXs without FSTs; a dialog pops up on upload to allow customizing the various properties, with presets for Mixamo/Autodesk avatar generators. Also enforces texture limits and adds joint indices.
This commit is contained in:
commit
f6875b5028
10 changed files with 477 additions and 117 deletions
|
@ -9,21 +9,29 @@
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QDoubleSpinBox>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#include <QFormLayout>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
#include <QHttpMultiPart>
|
#include <QHttpMultiPart>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QLineEdit>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QProgressBar>
|
#include <QProgressBar>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QTemporaryFile>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
#include <AccountManager.h>
|
#include <AccountManager.h>
|
||||||
|
|
||||||
#include <FBXReader.h>
|
|
||||||
|
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
#include "ModelUploader.h"
|
#include "ModelUploader.h"
|
||||||
|
|
||||||
|
@ -32,6 +40,10 @@ static const QString NAME_FIELD = "name";
|
||||||
static const QString FILENAME_FIELD = "filename";
|
static const QString FILENAME_FIELD = "filename";
|
||||||
static const QString TEXDIR_FIELD = "texdir";
|
static const QString TEXDIR_FIELD = "texdir";
|
||||||
static const QString LOD_FIELD = "lod";
|
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 S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com";
|
||||||
static const QString DATA_SERVER_URL = "https://data-web.highfidelity.io";
|
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 QString SETTING_NAME = "LastModelUploadLocation";
|
||||||
|
|
||||||
static const int MAX_SIZE = 10 * 1024 * 1024; // 10 MB
|
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 TIMEOUT = 1000;
|
||||||
static const int MAX_CHECK = 30;
|
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 (filename == "") {
|
||||||
// If the user canceled we return.
|
// If the user canceled we return.
|
||||||
Application::getInstance()->unlockSettings();
|
Application::getInstance()->unlockSettings();
|
||||||
|
@ -85,89 +99,160 @@ bool ModelUploader::zip() {
|
||||||
settings->setValue(SETTING_NAME, filename);
|
settings->setValue(SETTING_NAME, filename);
|
||||||
Application::getInstance()->unlockSettings();
|
Application::getInstance()->unlockSettings();
|
||||||
|
|
||||||
bool _nameIsPresent = false;
|
// First we check the FST file (if any)
|
||||||
QString texDir;
|
QFile* fst;
|
||||||
|
QVariantHash mapping;
|
||||||
|
QString basePath;
|
||||||
QString fbxFile;
|
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
|
// make sure we have some basic mappings
|
||||||
QFile fst(filename);
|
if (!mapping.contains(NAME_FIELD)) {
|
||||||
if (!fst.open(QFile::ReadOnly | QFile::Text)) {
|
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,
|
QMessageBox::warning(NULL,
|
||||||
QString("ModelUploader::zip()"),
|
QString("ModelUploader::zip()"),
|
||||||
QString("Could not open FST file."),
|
QString("Model name is missing in the .fst file."),
|
||||||
QMessageBox::Ok);
|
QMessageBox::Ok);
|
||||||
qDebug() << "[Warning] " << QString("Could not open FST file.");
|
qDebug() << "[Warning] " << QString("Model name is missing in the .fst file.");
|
||||||
return false;
|
|
||||||
}
|
|
||||||
qDebug() << "Reading FST file : " << QFileInfo(fst).filePath();
|
|
||||||
|
|
||||||
// Compress and copy the fst
|
|
||||||
if (!addPart(QFileInfo(fst).filePath(), QString("fst"))) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's read through the FST file
|
QByteArray texdirField = mapping.value(TEXDIR_FIELD).toByteArray();
|
||||||
QTextStream stream(&fst);
|
QString texDir;
|
||||||
QList<QString> line;
|
if (!texdirField.isEmpty()) {
|
||||||
while (!stream.atEnd()) {
|
texDir = basePath + "/" + texdirField;
|
||||||
line = stream.readLine().split(QRegExp("[ =]"), QString::SkipEmptyParts);
|
QFileInfo texInfo(texDir);
|
||||||
if (line.isEmpty()) {
|
if (!texInfo.exists() || !texInfo.isDir()) {
|
||||||
continue;
|
QMessageBox::warning(NULL,
|
||||||
}
|
QString("ModelUploader::zip()"),
|
||||||
|
QString("Texture directory could not be found."),
|
||||||
// according to what is read, we modify the command
|
QMessageBox::Ok);
|
||||||
if (line[0] == NAME_FIELD) {
|
qDebug() << "[Warning] " << QString("Texture directory could not be found.");
|
||||||
QHttpPart textPart;
|
return false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,15 +266,6 @@ bool ModelUploader::zip() {
|
||||||
}
|
}
|
||||||
_dataMultiPart->append(textPart);
|
_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;
|
_readyToSend = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -350,27 +426,18 @@ void ModelUploader::processCheck() {
|
||||||
delete reply;
|
delete reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) {
|
bool ModelUploader::addTextures(const QString& texdir, const FBXGeometry& geometry) {
|
||||||
QFile fbx(fbxFile);
|
|
||||||
if (!fbx.open(QIODevice::ReadOnly)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray buffer = fbx.readAll();
|
|
||||||
QVariantHash variantHash = readMapping(buffer);
|
|
||||||
FBXGeometry geometry = readFBX(buffer, variantHash);
|
|
||||||
|
|
||||||
foreach (FBXMesh mesh, geometry.meshes) {
|
foreach (FBXMesh mesh, geometry.meshes) {
|
||||||
foreach (FBXMeshPart part, mesh.parts) {
|
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,
|
if (!addPart(texdir + "/" + part.diffuseTexture.filename,
|
||||||
QString("texture%1").arg(++_texturesCount))) {
|
QString("texture%1").arg(++_texturesCount), true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!part.normalTexture.filename.isEmpty()) {
|
if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty()) {
|
||||||
if (!addPart(texdir + "/" + part.normalTexture.filename,
|
if (!addPart(texdir + "/" + part.normalTexture.filename,
|
||||||
QString("texture%1").arg(++_texturesCount))) {
|
QString("texture%1").arg(++_texturesCount), true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -380,7 +447,7 @@ bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) {
|
||||||
return true;
|
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);
|
QFile file(path);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
QMessageBox::warning(NULL,
|
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);
|
qDebug() << "[Warning] " << QString("Could not open %1").arg(path);
|
||||||
return false;
|
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.
|
// 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.
|
// 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.";
|
qDebug() << "File " << QFileInfo(file).fileName() << " added to model.";
|
||||||
_totalSize += file.size();
|
_totalSize += recodedContents.size();
|
||||||
if (_totalSize > MAX_SIZE) {
|
if (_totalSize > MAX_SIZE) {
|
||||||
QMessageBox::warning(NULL,
|
QMessageBox::warning(NULL,
|
||||||
QString("ModelUploader::zip()"),
|
QString("ModelUploader::zip()"),
|
||||||
|
@ -420,8 +509,165 @@ bool ModelUploader::addPart(const QString &path, const QString& name) {
|
||||||
return true;
|
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<QComboBox*>(_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,19 @@
|
||||||
#ifndef hifi_ModelUploader_h
|
#ifndef hifi_ModelUploader_h
|
||||||
#define hifi_ModelUploader_h
|
#define hifi_ModelUploader_h
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
class QDialog;
|
#include <FBXReader.h>
|
||||||
|
|
||||||
|
class QComboBox;
|
||||||
|
class QDoubleSpinBox;
|
||||||
class QFileInfo;
|
class QFileInfo;
|
||||||
class QHttpMultiPart;
|
class QHttpMultiPart;
|
||||||
|
class QLineEdit;
|
||||||
class QProgressBar;
|
class QProgressBar;
|
||||||
|
class QPushButton;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
class ModelUploader : public QObject {
|
class ModelUploader : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -56,8 +63,46 @@ private:
|
||||||
|
|
||||||
|
|
||||||
bool zip();
|
bool zip();
|
||||||
bool addTextures(const QString& texdir, const QString fbxFile);
|
bool addTextures(const QString& texdir, const FBXGeometry& geometry);
|
||||||
bool addPart(const QString& path, const QString& name);
|
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
|
#endif // hifi_ModelUploader_h
|
||||||
|
|
|
@ -517,7 +517,7 @@ void MyAvatar::loadData(QSettings* settings) {
|
||||||
setScale(_scale);
|
setScale(_scale);
|
||||||
Application::getInstance()->getCamera()->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());
|
setSkeletonModelURL(settings->value("skeletonModelURL").toUrl());
|
||||||
setDisplayName(settings->value("displayName").toString());
|
setDisplayName(settings->value("displayName").toString());
|
||||||
|
|
||||||
|
|
|
@ -305,7 +305,6 @@ void GeometryCache::setBlendedVertices(const QPointer<Model>& model, const QWeak
|
||||||
|
|
||||||
QSharedPointer<Resource> GeometryCache::createResource(const QUrl& url,
|
QSharedPointer<Resource> GeometryCache::createResource(const QUrl& url,
|
||||||
const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) {
|
const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) {
|
||||||
|
|
||||||
QSharedPointer<NetworkGeometry> geometry(new NetworkGeometry(url, fallback.staticCast<NetworkGeometry>(), delayLoad),
|
QSharedPointer<NetworkGeometry> geometry(new NetworkGeometry(url, fallback.staticCast<NetworkGeometry>(), delayLoad),
|
||||||
&Resource::allReferencesCleared);
|
&Resource::allReferencesCleared);
|
||||||
geometry->setLODParent(geometry);
|
geometry->setLODParent(geometry);
|
||||||
|
@ -320,6 +319,20 @@ NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer<NetworkGe
|
||||||
_mapping(mapping),
|
_mapping(mapping),
|
||||||
_textureBase(textureBase.isValid() ? textureBase : url),
|
_textureBase(textureBase.isValid() ? textureBase : url),
|
||||||
_fallback(fallback) {
|
_fallback(fallback) {
|
||||||
|
|
||||||
|
if (url.isEmpty()) {
|
||||||
|
// make the minimal amount of dummy geometry to satisfy Model
|
||||||
|
FBXJoint joint = { false, QVector<int>(), -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 {
|
bool NetworkGeometry::isLoadedWithTextures() const {
|
||||||
|
|
|
@ -44,7 +44,8 @@ Model::Model(QObject* parent) :
|
||||||
_boundingShape(),
|
_boundingShape(),
|
||||||
_boundingShapeLocalOffset(0.f),
|
_boundingShapeLocalOffset(0.f),
|
||||||
_lodDistance(0.0f),
|
_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
|
// we may have been created in the network thread, but we live in the main thread
|
||||||
moveToThread(Application::getInstance()->thread());
|
moveToThread(Application::getInstance()->thread());
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ public:
|
||||||
|
|
||||||
bool isActive() const { return _geometry && _geometry->isLoaded(); }
|
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(); }
|
bool isLoadedWithTextures() const { return _geometry && _geometry->isLoadedWithTextures(); }
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,8 @@ AvatarData::AvatarData() :
|
||||||
_isChatCirclingEnabled(false),
|
_isChatCirclingEnabled(false),
|
||||||
_hasNewJointRotations(true),
|
_hasNewJointRotations(true),
|
||||||
_headData(NULL),
|
_headData(NULL),
|
||||||
_handData(NULL),
|
_handData(NULL),
|
||||||
|
_faceModelURL("http://invalid.com"),
|
||||||
_displayNameBoundingRect(),
|
_displayNameBoundingRect(),
|
||||||
_displayNameTargetAlpha(0.0f),
|
_displayNameTargetAlpha(0.0f),
|
||||||
_displayNameAlpha(0.0f),
|
_displayNameAlpha(0.0f),
|
||||||
|
@ -640,7 +641,7 @@ bool AvatarData::hasBillboardChangedAfterParsing(const QByteArray& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarData::setFaceModelURL(const QUrl& faceModelURL) {
|
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();
|
qDebug() << "Changing face model for avatar to" << _faceModelURL.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -584,13 +584,16 @@ public:
|
||||||
};
|
};
|
||||||
|
|
||||||
glm::mat4 getGlobalTransform(const QMultiHash<QString, QString>& parentMap,
|
glm::mat4 getGlobalTransform(const QMultiHash<QString, QString>& parentMap,
|
||||||
const QHash<QString, FBXModel>& models, QString nodeID) {
|
const QHash<QString, FBXModel>& models, QString nodeID, bool mixamoHack) {
|
||||||
glm::mat4 globalTransform;
|
glm::mat4 globalTransform;
|
||||||
while (!nodeID.isNull()) {
|
while (!nodeID.isNull()) {
|
||||||
const FBXModel& model = models.value(nodeID);
|
const FBXModel& model = models.value(nodeID);
|
||||||
globalTransform = glm::translate(model.translation) * model.preTransform * glm::mat4_cast(model.preRotation *
|
globalTransform = glm::translate(model.translation) * model.preTransform * glm::mat4_cast(model.preRotation *
|
||||||
model.rotation * model.postRotation) * model.postTransform * globalTransform;
|
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<QString> parentIDs = parentMap.values(nodeID);
|
QList<QString> parentIDs = parentMap.values(nodeID);
|
||||||
nodeID = QString();
|
nodeID = QString();
|
||||||
foreach (const QString& parentID, parentIDs) {
|
foreach (const QString& parentID, parentIDs) {
|
||||||
|
@ -1006,9 +1009,25 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QMultiHash<QString, WeightedIndex> blendshapeChannelIndices;
|
QMultiHash<QString, WeightedIndex> blendshapeChannelIndices;
|
||||||
|
|
||||||
|
FBXGeometry geometry;
|
||||||
foreach (const FBXNode& child, node.children) {
|
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) {
|
foreach (const FBXNode& object, child.children) {
|
||||||
if (object.name == "Geometry") {
|
if (object.name == "Geometry") {
|
||||||
if (object.properties.at(2) == "Mesh") {
|
if (object.properties.at(2) == "Mesh") {
|
||||||
|
@ -1317,7 +1336,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get offset transform from mapping
|
// get offset transform from mapping
|
||||||
FBXGeometry geometry;
|
|
||||||
float offsetScale = mapping.value("scale", 1.0f).toFloat();
|
float offsetScale = mapping.value("scale", 1.0f).toFloat();
|
||||||
glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(),
|
glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(),
|
||||||
mapping.value("ry").toFloat(), mapping.value("rz").toFloat())));
|
mapping.value("ry").toFloat(), mapping.value("rz").toFloat())));
|
||||||
|
@ -1466,7 +1484,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping)
|
||||||
|
|
||||||
// accumulate local transforms
|
// accumulate local transforms
|
||||||
QString modelID = models.contains(it.key()) ? it.key() : parentMap.value(it.key());
|
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
|
// compute the mesh extents from the transformed vertices
|
||||||
foreach (const glm::vec3& vertex, extracted.mesh.vertices) {
|
foreach (const glm::vec3& vertex, extracted.mesh.vertices) {
|
||||||
|
@ -1803,6 +1821,33 @@ QVariantHash readMapping(const QByteArray& data) {
|
||||||
return parseMapping(&buffer);
|
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) {
|
FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping) {
|
||||||
QBuffer buffer(const_cast<QByteArray*>(&model));
|
QBuffer buffer(const_cast<QByteArray*>(&model));
|
||||||
buffer.open(QIODevice::ReadOnly);
|
buffer.open(QIODevice::ReadOnly);
|
||||||
|
|
|
@ -174,6 +174,8 @@ public:
|
||||||
class FBXGeometry {
|
class FBXGeometry {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
QString applicationName; ///< the name of the application that generated the model
|
||||||
|
|
||||||
QVector<FBXJoint> joints;
|
QVector<FBXJoint> joints;
|
||||||
QHash<QString, int> jointIndices; ///< 1-based, so as to more easily detect missing indices
|
QHash<QString, int> 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.
|
/// Reads an FST mapping from the supplied data.
|
||||||
QVariantHash readMapping(const QByteArray& 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.
|
/// Reads FBX geometry from the supplied model and mapping data.
|
||||||
/// \exception QString if an error occurs in parsing
|
/// \exception QString if an error occurs in parsing
|
||||||
FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping);
|
FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping);
|
||||||
|
|
|
@ -31,7 +31,7 @@ ResourceCache::~ResourceCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
QSharedPointer<Resource> ResourceCache::getResource(const QUrl& url, const QUrl& fallback, bool delayLoad, void* extra) {
|
QSharedPointer<Resource> 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);
|
return getResource(fallback, QUrl(), delayLoad);
|
||||||
}
|
}
|
||||||
QSharedPointer<Resource> resource = _resources.value(url);
|
QSharedPointer<Resource> resource = _resources.value(url);
|
||||||
|
@ -114,7 +114,11 @@ Resource::Resource(const QUrl& url, bool delayLoad) :
|
||||||
_reply(NULL),
|
_reply(NULL),
|
||||||
_attempts(0) {
|
_attempts(0) {
|
||||||
|
|
||||||
if (!(url.isValid() && ResourceCache::getNetworkAccessManager())) {
|
if (url.isEmpty()) {
|
||||||
|
_startedLoading = _loaded = true;
|
||||||
|
return;
|
||||||
|
|
||||||
|
} else if (!(url.isValid() && ResourceCache::getNetworkAccessManager())) {
|
||||||
_startedLoading = _failedToLoad = true;
|
_startedLoading = _failedToLoad = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue