overte/interface/src/ModelPackager.cpp
Marcel Verhagen 8f0893ba21 Added fileOnUrl to check if a texture exist at the location. It return the correct filename of where the texture lives.
Added the url of the fix file to extractFBXGeometry and readFBX and updated the calls to readFBX to include the url of the fix file.

So it now does not break existing content.

Found a second place in the FBXReader.cpp where the RelativeFileName stripped out the dir location.
2015-07-22 22:34:45 +02:00

415 lines
18 KiB
C++

//
// ModelPackager.cpp
//
//
// Created by Clement on 3/9/15.
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QFile>
#include <QFileDialog>
#include <QMessageBox>
#include <QTemporaryDir>
#include <FSTReader.h>
#include "ModelSelector.h"
#include "ModelPropertiesDialog.h"
#include "InterfaceLogging.h"
#include "ModelPackager.h"
static const int MAX_TEXTURE_SIZE = 1024;
void copyDirectoryContent(QDir& from, QDir& to) {
for (auto entry : from.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot |
QDir::NoSymLinks | QDir::Readable)) {
if (entry.isDir()) {
to.mkdir(entry.fileName());
from.cd(entry.fileName());
to.cd(entry.fileName());
copyDirectoryContent(from, to);
from.cdUp();
to.cdUp();
} else { // Files
QFile file(entry.absoluteFilePath());
QString newPath = to.absolutePath() + "/" + entry.fileName();
if (to.exists(entry.fileName())) {
QFile overridenFile(newPath);
overridenFile.remove();
}
file.copy(newPath);
}
}
}
bool ModelPackager::package() {
ModelPackager packager;
if (!packager.selectModel()) {
return false;
}
if (!packager.loadModel()) {
return false;
}
if (!packager.editProperties()) {
return false;
}
if (!packager.zipModel()) {
return false;
}
return true;
}
bool ModelPackager::selectModel() {
ModelSelector selector;
if(selector.exec() == QDialog::Accepted) {
_modelFile = selector.getFileInfo();
_modelType = selector.getModelType();
return true;
}
return false;
}
bool ModelPackager::loadModel() {
// First we check the FST file (if any)
if (_modelFile.completeSuffix().contains("fst")) {
QFile fst(_modelFile.filePath());
if (!fst.open(QFile::ReadOnly | QFile::Text)) {
QMessageBox::warning(NULL,
QString("ModelPackager::loadModel()"),
QString("Could not open FST file %1").arg(_modelFile.filePath()),
QMessageBox::Ok);
qWarning() << QString("ModelPackager::loadModel(): Could not open FST file %1").arg(_modelFile.filePath());
return false;
}
qCDebug(interfaceapp) << "Reading FST file : " << _modelFile.filePath();
_mapping = FSTReader::readMapping(fst.readAll());
fst.close();
_fbxInfo = QFileInfo(_modelFile.path() + "/" + _mapping.value(FILENAME_FIELD).toString());
} else {
_fbxInfo = QFileInfo(_modelFile.filePath());
}
// open the fbx file
QFile fbx(_fbxInfo.filePath());
if (!_fbxInfo.exists() || !_fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) {
QMessageBox::warning(NULL,
QString("ModelPackager::loadModel()"),
QString("Could not open FBX file %1").arg(_fbxInfo.filePath()),
QMessageBox::Ok);
qWarning() << QString("ModelPackager::loadModel(): Could not open FBX file %1").arg(_fbxInfo.filePath());
return false;
}
qCDebug(interfaceapp) << "Reading FBX file : " << _fbxInfo.filePath();
QByteArray fbxContents = fbx.readAll();
_geometry = readFBX(fbxContents, QVariantHash(), _fbxInfo.filePath());
// make sure we have some basic mappings
populateBasicMapping(_mapping, _fbxInfo.filePath(), _geometry);
return true;
}
bool ModelPackager::editProperties() {
// open the dialog to configure the rest
ModelPropertiesDialog properties(_modelType, _mapping, _modelFile.path(), _geometry);
if (properties.exec() == QDialog::Rejected) {
return false;
}
_mapping = properties.getMapping();
if (_modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL) {
// Make sure that a mapping for the root joint has been specified
QVariantHash joints = _mapping.value(JOINT_FIELD).toHash();
if (!joints.contains("jointRoot")) {
qWarning() << QString("%1 root joint not configured for skeleton.").arg(_modelFile.fileName());
QString message = "Your did not configure a root joint for your skeleton model.\n\nPackaging will be canceled.";
QMessageBox msgBox;
msgBox.setWindowTitle("Model Packager");
msgBox.setText(message);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setIcon(QMessageBox::Warning);
msgBox.exec();
return false;
}
}
return true;
}
bool ModelPackager::zipModel() {
QTemporaryDir dir;
dir.setAutoRemove(true);
QDir tempDir(dir.path());
QByteArray nameField = _mapping.value(NAME_FIELD).toByteArray();
tempDir.mkpath(nameField + "/textures");
QDir fbxDir(tempDir.path() + "/" + nameField);
QDir texDir(fbxDir.path() + "/textures");
// Copy textures
listTextures();
if (!_textures.empty()) {
QByteArray texdirField = _mapping.value(TEXDIR_FIELD).toByteArray();
_texDir = _modelFile.path() + "/" + texdirField;
copyTextures(_texDir, texDir);
}
// Copy LODs
QVariantHash lodField = _mapping.value(LOD_FIELD).toHash();
if (!lodField.empty()) {
for (auto it = lodField.constBegin(); it != lodField.constEnd(); ++it) {
QString oldPath = _modelFile.path() + "/" + it.key();
QFile lod(oldPath);
QString newPath = fbxDir.path() + "/" + QFileInfo(lod).fileName();
if (lod.exists()) {
lod.copy(newPath);
}
}
}
// Copy FBX
QFile fbx(_fbxInfo.filePath());
QByteArray filenameField = _mapping.value(FILENAME_FIELD).toByteArray();
QString newPath = fbxDir.path() + "/" + QFileInfo(filenameField).fileName();
fbx.copy(newPath);
// Correct FST
_mapping[FILENAME_FIELD] = tempDir.relativeFilePath(newPath);
_mapping[TEXDIR_FIELD] = tempDir.relativeFilePath(texDir.path());
// Copy FST
QFile fst(tempDir.path() + "/" + nameField + ".fst");
if (fst.open(QIODevice::WriteOnly)) {
fst.write(FSTReader::writeMapping(_mapping));
fst.close();
} else {
qCDebug(interfaceapp) << "Couldn't write FST file" << fst.fileName();
return false;
}
QString saveDirPath = QFileDialog::getExistingDirectory(nullptr, "Save Model",
"", QFileDialog::ShowDirsOnly);
if (saveDirPath.isEmpty()) {
qCDebug(interfaceapp) << "Invalid directory" << saveDirPath;
return false;
}
QDir saveDir(saveDirPath);
copyDirectoryContent(tempDir, saveDir);
return true;
}
void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry) {
bool isBodyType = _modelType == FSTReader::BODY_ONLY_MODEL || _modelType == FSTReader::HEAD_AND_BODY_MODEL;
// mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will
// be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file
bool likelyMixamoFile = geometry.applicationName == "mixamo.com" ||
(geometry.blendshapeChannelNames.contains("BrowsDown_Right") &&
geometry.blendshapeChannelNames.contains("MouthOpen") &&
geometry.blendshapeChannelNames.contains("Blink_Left") &&
geometry.blendshapeChannelNames.contains("Blink_Right") &&
geometry.blendshapeChannelNames.contains("Squint_Right"));
if (!mapping.contains(NAME_FIELD)) {
mapping.insert(NAME_FIELD, QFileInfo(filename).baseName());
}
if (!mapping.contains(FILENAME_FIELD)) {
QDir root(_modelFile.path());
mapping.insert(FILENAME_FIELD, root.relativeFilePath(filename));
}
if (!mapping.contains(TEXDIR_FIELD)) {
mapping.insert(TEXDIR_FIELD, ".");
}
// mixamo/autodesk defaults
if (!mapping.contains(SCALE_FIELD)) {
mapping.insert(SCALE_FIELD, 1.0);
}
QVariantHash joints = mapping.value(JOINT_FIELD).toHash();
if (!joints.contains("jointEyeLeft")) {
joints.insert("jointEyeLeft", geometry.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" :
(geometry.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye"));
}
if (!joints.contains("jointEyeRight")) {
joints.insert("jointEyeRight", geometry.jointIndices.contains("jointEyeRight") ? "jointEyeRight" :
geometry.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye");
}
if (!joints.contains("jointNeck")) {
joints.insert("jointNeck", geometry.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck");
}
if (isBodyType) {
if (!joints.contains("jointRoot")) {
joints.insert("jointRoot", "Hips");
}
if (!joints.contains("jointLean")) {
joints.insert("jointLean", "Spine");
}
if (!joints.contains("jointLeftHand")) {
joints.insert("jointLeftHand", "LeftHand");
}
if (!joints.contains("jointRightHand")) {
joints.insert("jointRightHand", "RightHand");
}
}
if (!joints.contains("jointHead")) {
const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd";
joints.insert("jointHead", geometry.jointIndices.contains(topName) ? topName : "Head");
}
mapping.insert(JOINT_FIELD, joints);
if (isBodyType) {
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");
}
}
// If there are no blendshape mappings, and we detect that this is likely a mixamo file,
// then we can add the default mixamo to "faceshift" mappings
if (!mapping.contains(BLENDSHAPE_FIELD) && likelyMixamoFile) {
QVariantHash blendshapes;
blendshapes.insertMulti("BrowsD_L", QVariantList() << "BrowsDown_Left" << 1.0);
blendshapes.insertMulti("BrowsD_R", QVariantList() << "BrowsDown_Right" << 1.0);
blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Left" << 1.0);
blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Right" << 1.0);
blendshapes.insertMulti("BrowsU_L", QVariantList() << "BrowsUp_Left" << 1.0);
blendshapes.insertMulti("BrowsU_R", QVariantList() << "BrowsUp_Right" << 1.0);
blendshapes.insertMulti("ChinLowerRaise", QVariantList() << "Jaw_Up" << 1.0);
blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Left" << 0.5);
blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Right" << 0.5);
blendshapes.insertMulti("EyeBlink_L", QVariantList() << "Blink_Left" << 1.0);
blendshapes.insertMulti("EyeBlink_R", QVariantList() << "Blink_Right" << 1.0);
blendshapes.insertMulti("EyeOpen_L", QVariantList() << "EyesWide_Left" << 1.0);
blendshapes.insertMulti("EyeOpen_R", QVariantList() << "EyesWide_Right" << 1.0);
blendshapes.insertMulti("EyeSquint_L", QVariantList() << "Squint_Left" << 1.0);
blendshapes.insertMulti("EyeSquint_R", QVariantList() << "Squint_Right" << 1.0);
blendshapes.insertMulti("JawFwd", QVariantList() << "JawForeward" << 1.0);
blendshapes.insertMulti("JawLeft", QVariantList() << "JawRotateY_Left" << 0.5);
blendshapes.insertMulti("JawOpen", QVariantList() << "MouthOpen" << 0.7);
blendshapes.insertMulti("JawRight", QVariantList() << "Jaw_Right" << 1.0);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "JawForeward" << 0.39);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "Jaw_Down" << 0.36);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Left" << 1.0);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Right" << 1.0);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Left" << 0.5);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Right" << 0.5);
blendshapes.insertMulti("LipsFunnel", QVariantList() << "TongueUp" << 1.0);
blendshapes.insertMulti("LipsLowerClose", QVariantList() << "LowerLipIn" << 1.0);
blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Left" << 0.7);
blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Right" << 0.7);
blendshapes.insertMulti("LipsLowerOpen", QVariantList() << "LowerLipOut" << 1.0);
blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Left" << 1.0);
blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Right" << 1.0);
blendshapes.insertMulti("LipsUpperClose", QVariantList() << "UpperLipIn" << 1.0);
blendshapes.insertMulti("LipsUpperOpen", QVariantList() << "UpperLipOut" << 1.0);
blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Left" << 0.7);
blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Right" << 0.7);
blendshapes.insertMulti("MouthDimple_L", QVariantList() << "Smile_Left" << 0.25);
blendshapes.insertMulti("MouthDimple_R", QVariantList() << "Smile_Right" << 0.25);
blendshapes.insertMulti("MouthFrown_L", QVariantList() << "Frown_Left" << 1.0);
blendshapes.insertMulti("MouthFrown_R", QVariantList() << "Frown_Right" << 1.0);
blendshapes.insertMulti("MouthLeft", QVariantList() << "Midmouth_Left" << 1.0);
blendshapes.insertMulti("MouthRight", QVariantList() << "Midmouth_Right" << 1.0);
blendshapes.insertMulti("MouthSmile_L", QVariantList() << "Smile_Left" << 1.0);
blendshapes.insertMulti("MouthSmile_R", QVariantList() << "Smile_Right" << 1.0);
blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Left" << 1.0);
blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Right" << 1.0);
blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Left" << 0.75);
blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Right" << 0.75);
blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Left" << 0.5);
blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Right" << 0.5);
mapping.insert(BLENDSHAPE_FIELD, blendshapes);
}
}
void ModelPackager::listTextures() {
_textures.clear();
foreach (FBXMesh mesh, _geometry.meshes) {
foreach (FBXMeshPart part, mesh.parts) {
if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty() &&
!_textures.contains(part.diffuseTexture.filename)) {
_textures << part.diffuseTexture.filename;
}
if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty() &&
!_textures.contains(part.normalTexture.filename)) {
_textures << part.normalTexture.filename;
}
if (!part.specularTexture.filename.isEmpty() && part.specularTexture.content.isEmpty() &&
!_textures.contains(part.specularTexture.filename)) {
_textures << part.specularTexture.filename;
}
if (!part.emissiveTexture.filename.isEmpty() && part.emissiveTexture.content.isEmpty() &&
!_textures.contains(part.emissiveTexture.filename)) {
_textures << part.emissiveTexture.filename;
}
}
}
}
bool ModelPackager::copyTextures(const QString& oldDir, const QDir& newDir) {
QString errors;
for (auto texture : _textures) {
QString oldPath = oldDir + "/" + texture;
QString newPath = newDir.path() + "/" + texture;
// Make sure path exists
if (texture.contains("/")) {
QString dirPath = newDir.relativeFilePath(QFileInfo(newPath).path());
newDir.mkpath(dirPath);
}
QFile texFile(oldPath);
if (texFile.exists() && texFile.open(QIODevice::ReadOnly)) {
// Check if texture needs to be recoded
QFileInfo fileInfo(oldPath);
QString extension = fileInfo.suffix().toLower();
bool isJpeg = (extension == "jpg");
bool mustRecode = !(isJpeg || extension == "png");
QImage image = QImage::fromData(texFile.readAll());
// Recode texture if too big
if (image.width() > MAX_TEXTURE_SIZE || image.height() > MAX_TEXTURE_SIZE) {
image = image.scaled(MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, Qt::KeepAspectRatio);
mustRecode = true;
}
// Copy texture
if (mustRecode) {
QFile newTexFile(newPath);
newTexFile.open(QIODevice::WriteOnly);
image.save(&newTexFile, isJpeg ? "JPG" : "PNG");
} else {
texFile.copy(newPath);
}
} else {
errors += QString("\n%1").arg(oldPath);
}
}
if (!errors.isEmpty()) {
QMessageBox::warning(nullptr, "ModelPackager::copyTextures()",
"Missing textures:" + errors);
qCDebug(interfaceapp) << "ModelPackager::copyTextures():" << errors;
return false;
}
return true;
}