mirror of
https://github.com/overte-org/overte.git
synced 2025-07-10 20:38:37 +02:00
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.
415 lines
18 KiB
C++
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;
|
|
}
|
|
|
|
|