overte/libraries/fbx/src/FBXSerializer.cpp
2019-10-18 11:30:48 -07:00

1753 lines
94 KiB
C++

//
// FBXSerializer.cpp
// libraries/fbx/src
//
// Created by Andrzej Kapolka on 9/18/13.
// Copyright 2013 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 "FBXSerializer.h"
#include <QBuffer>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/transform.hpp>
#include <FaceshiftConstants.h>
#include <hfm/ModelFormatLogging.h>
#include <hfm/HFMModelMath.h>
// TOOL: Uncomment the following line to enable the filtering of all the unkwnon fields of a node so we can break point easily while loading a model with problems...
//#define DEBUG_FBXSERIALIZER
using namespace std;
glm::vec3 parseVec3(const QString& string) {
QStringList elements = string.split(',');
if (elements.isEmpty()) {
return glm::vec3();
}
glm::vec3 value;
for (int i = 0; i < 3; i++) {
// duplicate last value if there aren't three elements
value[i] = elements.at(min(i, elements.size() - 1)).trimmed().toFloat();
}
return value;
}
enum RotationOrder {
OrderXYZ = 0,
OrderXZY,
OrderYZX,
OrderYXZ,
OrderZXY,
OrderZYX,
OrderSphericXYZ
};
bool haveReportedUnhandledRotationOrder = false; // Report error only once per FBX file.
glm::vec3 convertRotationToXYZ(int rotationOrder, const glm::vec3& rotation) {
// Convert rotation with given rotation order to have order XYZ.
if (rotationOrder == OrderXYZ) {
return rotation;
}
glm::quat xyzRotation;
switch (rotationOrder) {
case OrderXZY:
xyzRotation = glm::quat(glm::radians(glm::vec3(0, rotation.y, 0)))
* (glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))));
break;
case OrderYZX:
xyzRotation = glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0)))
* (glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))));
break;
case OrderYXZ:
xyzRotation = glm::quat(glm::radians(glm::vec3(0, 0, rotation.z)))
* (glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))));
break;
case OrderZXY:
xyzRotation = glm::quat(glm::radians(glm::vec3(0, rotation.y, 0)))
* (glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))));
break;
case OrderZYX:
xyzRotation = glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0)))
* (glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))) * glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))));
break;
default:
// FIXME: Handle OrderSphericXYZ.
if (!haveReportedUnhandledRotationOrder) {
qCDebug(modelformat) << "ERROR: Unhandled rotation order in FBX file:" << rotationOrder;
haveReportedUnhandledRotationOrder = true;
}
return rotation;
}
return glm::degrees(safeEulerAngles(xyzRotation));
}
QString processID(const QString& id) {
// Blender (at least) prepends a type to the ID, so strip it out
return id.mid(id.lastIndexOf(':') + 1);
}
QString getModelName(const QVariantList& properties) {
QString name;
if (properties.size() == 3) {
name = properties.at(1).toString();
name = processID(name.left(name.indexOf(QChar('\0'))));
} else {
name = processID(properties.at(0).toString());
}
return name;
}
QString getMaterialName(const QVariantList& properties) {
QString name;
if (properties.size() == 1 || properties.at(1).toString().isEmpty()) {
name = properties.at(0).toString();
name = processID(name.left(name.indexOf(QChar('\0'))));
} else {
name = processID(properties.at(1).toString());
}
return name;
}
QString getID(const QVariantList& properties, int index = 0) {
return processID(properties.at(index).toString());
}
class FBXModel {
public:
QString name;
int parentIndex;
glm::vec3 translation;
glm::mat4 preTransform;
glm::quat preRotation;
glm::quat rotation;
glm::quat postRotation;
glm::mat4 postTransform;
glm::vec3 rotationMin; // radians
glm::vec3 rotationMax; // radians
bool hasGeometricOffset;
glm::vec3 geometricTranslation;
glm::quat geometricRotation;
glm::vec3 geometricScaling;
bool isLimbNode; // is this FBXModel transform is a "LimbNode" i.e. a joint
};
glm::mat4 getGlobalTransform(const QMultiMap<QString, QString>& _connectionParentMap,
const QHash<QString, FBXModel>& fbxModels, QString nodeID, bool mixamoHack, const QString& url) {
glm::mat4 globalTransform;
QVector<QString> visitedNodes; // Used to prevent following a cycle
while (!nodeID.isNull()) {
visitedNodes.append(nodeID); // Append each node we visit
const FBXModel& fbxModel = fbxModels.value(nodeID);
globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation *
fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform;
if (fbxModel.hasGeometricOffset) {
glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(fbxModel.geometricScaling, fbxModel.geometricRotation, fbxModel.geometricTranslation);
globalTransform = globalTransform * geometricOffset;
}
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 = _connectionParentMap.values(nodeID);
nodeID = QString();
foreach(const QString& parentID, parentIDs) {
if (visitedNodes.contains(parentID)) {
qCWarning(modelformat) << "Ignoring loop detected in FBX connection map for" << url;
continue;
}
if (fbxModels.contains(parentID)) {
nodeID = parentID;
break;
}
}
}
return globalTransform;
}
std::vector<QString> getModelIDsForMeshID(const QString& meshID, const QHash<QString, FBXModel>& fbxModels, const QMultiMap<QString, QString>& _connectionParentMap) {
std::vector<QString> modelsForMesh;
if (fbxModels.contains(meshID)) {
modelsForMesh.push_back(meshID);
} else {
// This mesh may have more than one parent model, with different material and transform, representing a different instance of the mesh
for (const auto& parentID : _connectionParentMap.values(meshID)) {
if (fbxModels.contains(parentID)) {
modelsForMesh.push_back(parentID);
}
}
}
return modelsForMesh;
}
class ExtractedBlendshape {
public:
QString id;
HFMBlendshape blendshape;
};
void printNode(const FBXNode& node, int indentLevel) {
int indentLength = 2;
hifi::ByteArray spaces(indentLevel * indentLength, ' ');
QDebug nodeDebug = qDebug(modelformat);
nodeDebug.nospace() << spaces.data() << node.name.data() << ": ";
foreach (const QVariant& property, node.properties) {
nodeDebug << property;
}
foreach (const FBXNode& child, node.children) {
printNode(child, indentLevel + 1);
}
}
class Cluster {
public:
QVector<int> indices;
QVector<double> weights;
glm::mat4 transformLink;
};
void appendModelIDs(const QString& parentID, const QMultiMap<QString, QString>& connectionChildMap,
QHash<QString, FBXModel>& fbxModels, QSet<QString>& remainingModels, QVector<QString>& modelIDs, bool isRootNode = false) {
if (remainingModels.contains(parentID)) {
modelIDs.append(parentID);
remainingModels.remove(parentID);
}
int parentIndex = isRootNode ? -1 : modelIDs.size() - 1;
foreach (const QString& childID, connectionChildMap.values(parentID)) {
if (remainingModels.contains(childID)) {
FBXModel& fbxModel = fbxModels[childID];
if (fbxModel.parentIndex == -1) {
fbxModel.parentIndex = parentIndex;
appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs);
}
}
}
}
HFMBlendshape extractBlendshape(const FBXNode& object) {
HFMBlendshape blendshape;
foreach (const FBXNode& data, object.children) {
if (data.name == "Indexes") {
blendshape.indices = FBXSerializer::getIntVector(data);
} else if (data.name == "Vertices") {
blendshape.vertices = FBXSerializer::createVec3Vector(FBXSerializer::getDoubleVector(data));
} else if (data.name == "Normals") {
blendshape.normals = FBXSerializer::createVec3Vector(FBXSerializer::getDoubleVector(data));
}
}
return blendshape;
}
QVector<int> getIndices(const QVector<QString> ids, QVector<QString> modelIDs) {
QVector<int> indices;
foreach (const QString& id, ids) {
int index = modelIDs.indexOf(id);
if (index != -1) {
indices.append(index);
}
}
return indices;
}
typedef QPair<int, float> WeightedIndex;
void addBlendshapes(const ExtractedBlendshape& extracted, const QList<WeightedIndex>& indices, ExtractedMesh& extractedMesh) {
foreach (const WeightedIndex& index, indices) {
extractedMesh.mesh.blendshapes.resize(max(extractedMesh.mesh.blendshapes.size(), index.first + 1));
extractedMesh.blendshapeIndexMaps.resize(extractedMesh.mesh.blendshapes.size());
HFMBlendshape& blendshape = extractedMesh.mesh.blendshapes[index.first];
QHash<int, int>& blendshapeIndexMap = extractedMesh.blendshapeIndexMaps[index.first];
for (int i = 0; i < extracted.blendshape.indices.size(); i++) {
int oldIndex = extracted.blendshape.indices.at(i);
for (QMultiHash<int, int>::const_iterator it = extractedMesh.newIndices.constFind(oldIndex);
it != extractedMesh.newIndices.constEnd() && it.key() == oldIndex; it++) {
QHash<int, int>::iterator blendshapeIndex = blendshapeIndexMap.find(it.value());
if (blendshapeIndex == blendshapeIndexMap.end()) {
blendshapeIndexMap.insert(it.value(), blendshape.indices.size());
blendshape.indices.append(it.value());
blendshape.vertices.append(extracted.blendshape.vertices.at(i) * index.second);
blendshape.normals.append(extracted.blendshape.normals.at(i) * index.second);
} else {
blendshape.vertices[*blendshapeIndex] += extracted.blendshape.vertices.at(i) * index.second;
blendshape.normals[*blendshapeIndex] += extracted.blendshape.normals.at(i) * index.second;
}
}
}
}
}
QString getTopModelID(const QMultiMap<QString, QString>& connectionParentMap,
const QHash<QString, FBXModel>& fbxModels, const QString& modelID, const QString& url) {
QString topID = modelID;
QVector<QString> visitedNodes; // Used to prevent following a cycle
forever {
visitedNodes.append(topID); // Append each node we visit
foreach (const QString& parentID, connectionParentMap.values(topID)) {
if (visitedNodes.contains(parentID)) {
qCWarning(modelformat) << "Ignoring loop detected in FBX connection map for" << url;
continue;
}
if (fbxModels.contains(parentID)) {
topID = parentID;
goto outerContinue;
}
}
return topID;
outerContinue: ;
}
}
QString getString(const QVariant& value) {
// if it's a list, return the first entry
QVariantList list = value.toList();
return list.isEmpty() ? value.toString() : list.at(0).toString();
}
class AnimationCurve {
public:
QVector<float> values;
};
bool checkMaterialsHaveTextures(const QHash<QString, HFMMaterial>& materials,
const QHash<QString, hifi::ByteArray>& textureFilenames, const QMultiMap<QString, QString>& _connectionChildMap) {
foreach (const QString& materialID, materials.keys()) {
foreach (const QString& childID, _connectionChildMap.values(materialID)) {
if (textureFilenames.contains(childID)) {
return true;
}
}
}
return false;
}
int matchTextureUVSetToAttributeChannel(const QString& texUVSetName, const QHash<QString, int>& texcoordChannels) {
if (texUVSetName.isEmpty()) {
return 0;
} else {
QHash<QString, int>::const_iterator tcUnit = texcoordChannels.find(texUVSetName);
if (tcUnit != texcoordChannels.end()) {
int channel = (*tcUnit);
if (channel >= 2) {
channel = 0;
}
return channel;
} else {
return 0;
}
}
}
HFMLight extractLight(const FBXNode& object) {
HFMLight light;
foreach (const FBXNode& subobject, object.children) {
QString childname = QString(subobject.name);
if (subobject.name == "Properties70") {
foreach (const FBXNode& property, subobject.children) {
int valIndex = 4;
QString propName = QString(property.name);
if (property.name == "P") {
QString propname = property.properties.at(0).toString();
if (propname == "Intensity") {
light.intensity = 0.01f * property.properties.at(valIndex).value<float>();
} else if (propname == "Color") {
light.color = FBXSerializer::getVec3(property.properties, valIndex);
}
}
}
} else if ( subobject.name == "GeometryVersion"
|| subobject.name == "TypeFlags") {
}
}
#if defined(DEBUG_FBXSERIALIZER)
QString type = object.properties.at(0).toString();
type = object.properties.at(1).toString();
type = object.properties.at(2).toString();
foreach (const QVariant& prop, object.properties) {
QString proptype = prop.typeName();
QString propval = prop.toString();
if (proptype == "Properties70") {
}
}
#endif
return light;
}
hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) {
// in order to match the behaviour when loading models from remote URLs
// we assume that all external textures are right beside the loaded model
// ignoring any relative paths or absolute paths inside of models
return filepath.mid(filepath.lastIndexOf('/') + 1);
}
HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) {
const FBXNode& node = _rootNode;
bool deduplicateIndices = mapping["deduplicateIndices"].toBool();
QMap<QString, ExtractedMesh> meshes;
QHash<QString, QString> modelIDsToNames;
QHash<QString, int> meshIDsToMeshIndices;
QHash<QString, QString> ooChildToParent;
QVector<ExtractedBlendshape> blendshapes;
QHash<QString, FBXModel> fbxModels;
QHash<QString, Cluster> fbxClusters;
QHash<QString, AnimationCurve> animationCurves;
QHash<QString, QString> typeFlags;
QHash<QString, QString> localRotations;
QHash<QString, QString> localTranslations;
QHash<QString, QString> xComponents;
QHash<QString, QString> yComponents;
QHash<QString, QString> zComponents;
std::map<QString, HFMLight> lights;
hifi::VariantHash blendshapeMappings = mapping.value("bs").toHash();
QMultiHash<hifi::ByteArray, WeightedIndex> blendshapeIndices;
for (int i = 0;; i++) {
hifi::ByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i];
if (blendshapeName.isEmpty()) {
break;
}
QList<QVariant> mappings = blendshapeMappings.values(blendshapeName);
if (mappings.isEmpty()) {
blendshapeIndices.insert(blendshapeName, WeightedIndex(i, 1.0f));
} else {
foreach (const QVariant& mapping, mappings) {
QVariantList blendshapeMapping = mapping.toList();
blendshapeIndices.insert(blendshapeMapping.at(0).toByteArray(),
WeightedIndex(i, blendshapeMapping.at(1).toFloat()));
}
}
}
QMultiHash<QString, WeightedIndex> blendshapeChannelIndices;
#if defined(DEBUG_FBXSERIALIZER)
int unknown = 0;
#endif
HFMModel* hfmModelPtr = new HFMModel;
HFMModel& hfmModel = *hfmModelPtr;
hfmModel.originalURL = url;
float unitScaleFactor = 1.0f;
glm::quat upAxisZRotation;
bool applyUpAxisZRotation = false;
glm::vec3 ambientColor;
QString hifiGlobalNodeID;
unsigned int meshIndex = 0;
haveReportedUnhandledRotationOrder = false;
int fbxVersionNumber = -1;
foreach (const FBXNode& child, node.children) {
if (child.name == "FBXHeaderExtension") {
foreach (const FBXNode& object, child.children) {
if (object.name == "SceneInfo") {
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == "MetaData") {
foreach (const FBXNode& subsubobject, subobject.children) {
if (subsubobject.name == "Author") {
hfmModel.author = subsubobject.properties.at(0).toString();
}
}
} else if (subobject.name == "Properties70") {
foreach (const FBXNode& subsubobject, subobject.children) {
static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName"));
if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 &&
subsubobject.properties.at(0) == APPLICATION_NAME) {
hfmModel.applicationName = subsubobject.properties.at(4).toString();
}
}
}
}
} else if (object.name == "FBXVersion") {
fbxVersionNumber = object.properties.at(0).toInt();
}
}
} else if (child.name == "GlobalSettings") {
foreach (const FBXNode& object, child.children) {
if (object.name == "Properties70") {
QString propertyName = "P";
int index = 4;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == propertyName) {
static const QVariant UNIT_SCALE_FACTOR = hifi::ByteArray("UnitScaleFactor");
static const QVariant AMBIENT_COLOR = hifi::ByteArray("AmbientColor");
static const QVariant UP_AXIS = hifi::ByteArray("UpAxis");
const auto& subpropName = subobject.properties.at(0);
if (subpropName == UNIT_SCALE_FACTOR) {
unitScaleFactor = subobject.properties.at(index).toFloat();
} else if (subpropName == AMBIENT_COLOR) {
ambientColor = getVec3(subobject.properties, index);
} else if (subpropName == UP_AXIS) {
constexpr int UP_AXIS_Y = 1;
constexpr int UP_AXIS_Z = 2;
int upAxis = subobject.properties.at(index).toInt();
if (upAxis == UP_AXIS_Y) {
// No update necessary, y up is the default
} else if (upAxis == UP_AXIS_Z) {
upAxisZRotation = glm::angleAxis(glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
applyUpAxisZRotation = true;
}
}
}
}
}
}
} else if (child.name == "Objects") {
foreach (const FBXNode& object, child.children) {
if (object.name == "Geometry") {
if (object.properties.at(2) == "Mesh") {
meshes.insert(getID(object.properties), extractMesh(object, meshIndex, deduplicateIndices));
} else { // object.properties.at(2) == "Shape"
ExtractedBlendshape blendshape = { getID(object.properties), extractBlendshape(object) };
blendshapes.append(blendshape);
}
} else if (object.name == "Model") {
QString name = getModelName(object.properties);
QString id = getID(object.properties);
modelIDsToNames.insert(id, name);
QString modelname = name.toLower();
if (modelname.startsWith("hifi")) {
hifiGlobalNodeID = id;
}
glm::vec3 translation;
// NOTE: the euler angles as supplied by the FBX file are in degrees
glm::vec3 rotationOffset;
int rotationOrder = OrderXYZ; // Default rotation order set in "Definitions" node is assumed to be XYZ.
glm::vec3 preRotation, rotation, postRotation;
glm::vec3 scale = glm::vec3(1.0f, 1.0f, 1.0f);
glm::vec3 scalePivot, rotationPivot, scaleOffset;
bool rotationMinX = false, rotationMinY = false, rotationMinZ = false;
bool rotationMaxX = false, rotationMaxY = false, rotationMaxZ = false;
// local offset transforms from 3ds max
bool hasGeometricOffset = false;
glm::vec3 geometricTranslation;
glm::vec3 geometricScaling(1.0f, 1.0f, 1.0f);
glm::vec3 geometricRotation;
glm::vec3 rotationMin, rotationMax;
bool isLimbNode = object.properties.size() >= 3 && object.properties.at(2) == "LimbNode";
FBXModel fbxModel = { name, -1, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(),
glm::mat4(), glm::vec3(), glm::vec3(),
false, glm::vec3(), glm::quat(), glm::vec3(1.0f), isLimbNode };
ExtractedMesh* mesh = NULL;
QVector<ExtractedBlendshape> blendshapes;
foreach (const FBXNode& subobject, object.children) {
bool properties = false;
hifi::ByteArray propertyName;
int index;
if (subobject.name == "Properties60") {
properties = true;
propertyName = "Property";
index = 3;
} else if (subobject.name == "Properties70") {
properties = true;
propertyName = "P";
index = 4;
}
if (properties) {
static const QVariant ROTATION_ORDER = hifi::ByteArray("RotationOrder");
static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation");
static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation");
static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling");
static const QVariant LCL_TRANSLATION = hifi::ByteArray("Lcl Translation");
static const QVariant LCL_ROTATION = hifi::ByteArray("Lcl Rotation");
static const QVariant LCL_SCALING = hifi::ByteArray("Lcl Scaling");
static const QVariant ROTATION_MAX = hifi::ByteArray("RotationMax");
static const QVariant ROTATION_MAX_X = hifi::ByteArray("RotationMaxX");
static const QVariant ROTATION_MAX_Y = hifi::ByteArray("RotationMaxY");
static const QVariant ROTATION_MAX_Z = hifi::ByteArray("RotationMaxZ");
static const QVariant ROTATION_MIN = hifi::ByteArray("RotationMin");
static const QVariant ROTATION_MIN_X = hifi::ByteArray("RotationMinX");
static const QVariant ROTATION_MIN_Y = hifi::ByteArray("RotationMinY");
static const QVariant ROTATION_MIN_Z = hifi::ByteArray("RotationMinZ");
static const QVariant ROTATION_OFFSET = hifi::ByteArray("RotationOffset");
static const QVariant ROTATION_PIVOT = hifi::ByteArray("RotationPivot");
static const QVariant SCALING_OFFSET = hifi::ByteArray("ScalingOffset");
static const QVariant SCALING_PIVOT = hifi::ByteArray("ScalingPivot");
static const QVariant PRE_ROTATION = hifi::ByteArray("PreRotation");
static const QVariant POST_ROTATION = hifi::ByteArray("PostRotation");
foreach(const FBXNode& property, subobject.children) {
const auto& childProperty = property.properties.at(0);
if (property.name == propertyName) {
if (childProperty == LCL_TRANSLATION) {
translation = getVec3(property.properties, index);
} else if (childProperty == ROTATION_ORDER) {
rotationOrder = property.properties.at(index).toInt();
} else if (childProperty == ROTATION_OFFSET) {
rotationOffset = getVec3(property.properties, index);
} else if (childProperty == ROTATION_PIVOT) {
rotationPivot = getVec3(property.properties, index);
} else if (childProperty == PRE_ROTATION) {
preRotation = convertRotationToXYZ(rotationOrder, getVec3(property.properties, index));
} else if (childProperty == LCL_ROTATION) {
rotation = convertRotationToXYZ(rotationOrder, getVec3(property.properties, index));
} else if (childProperty == POST_ROTATION) {
postRotation = convertRotationToXYZ(rotationOrder, getVec3(property.properties, index));
} else if (childProperty == SCALING_PIVOT) {
scalePivot = getVec3(property.properties, index);
} else if (childProperty == LCL_SCALING) {
scale = getVec3(property.properties, index);
} else if (childProperty == SCALING_OFFSET) {
scaleOffset = getVec3(property.properties, index);
// NOTE: these rotation limits are stored in degrees (NOT radians)
} else if (childProperty == ROTATION_MIN) {
rotationMin = getVec3(property.properties, index);
} else if (childProperty == ROTATION_MAX) {
rotationMax = getVec3(property.properties, index);
} else if (childProperty == ROTATION_MIN_X) {
rotationMinX = property.properties.at(index).toBool();
} else if (childProperty == ROTATION_MIN_Y) {
rotationMinY = property.properties.at(index).toBool();
} else if (childProperty == ROTATION_MIN_Z) {
rotationMinZ = property.properties.at(index).toBool();
} else if (childProperty == ROTATION_MAX_X) {
rotationMaxX = property.properties.at(index).toBool();
} else if (childProperty == ROTATION_MAX_Y) {
rotationMaxY = property.properties.at(index).toBool();
} else if (childProperty == ROTATION_MAX_Z) {
rotationMaxZ = property.properties.at(index).toBool();
} else if (childProperty == GEOMETRIC_TRANSLATION) {
geometricTranslation = getVec3(property.properties, index);
hasGeometricOffset = true;
} else if (childProperty == GEOMETRIC_ROTATION) {
geometricRotation = getVec3(property.properties, index);
hasGeometricOffset = true;
} else if (childProperty == GEOMETRIC_SCALING) {
geometricScaling = getVec3(property.properties, index);
hasGeometricOffset = true;
}
}
}
} else if (subobject.name == "Vertices" || subobject.name == "DracoMesh") {
// it's a mesh as well as a model
mesh = &meshes[getID(object.properties)];
*mesh = extractMesh(object, meshIndex, deduplicateIndices);
} else if (subobject.name == "Shape") {
ExtractedBlendshape blendshape = { subobject.properties.at(0).toString(),
extractBlendshape(subobject) };
blendshapes.append(blendshape);
}
#if defined(DEBUG_FBXSERIALIZER)
else if (subobject.name == "TypeFlags") {
QString attributetype = subobject.properties.at(0).toString();
if (!attributetype.empty()) {
if (attributetype == "Light") {
QString lightprop;
foreach (const QVariant& vprop, subobject.properties) {
lightprop = vprop.toString();
}
HFMLight light = extractLight(object);
}
}
} else {
QString whatisthat = subobject.name;
if (whatisthat == "Shape") {
}
}
#endif
}
// add the blendshapes included in the model, if any
if (mesh) {
foreach (const ExtractedBlendshape& blendshape, blendshapes) {
addBlendshapes(blendshape, blendshapeIndices.values(blendshape.id.toLatin1()), *mesh);
}
}
// see FBX documentation, http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html
fbxModel.translation = translation;
fbxModel.preTransform = glm::translate(rotationOffset) * glm::translate(rotationPivot);
fbxModel.preRotation = glm::quat(glm::radians(preRotation));
fbxModel.rotation = glm::quat(glm::radians(rotation));
fbxModel.postRotation = glm::inverse(glm::quat(glm::radians(postRotation)));
fbxModel.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) *
glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot);
// NOTE: angles from the FBX file are in degrees
// so we convert them to radians for the FBXModel class
fbxModel.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f,
rotationMinY ? rotationMin.y : -180.0f, rotationMinZ ? rotationMin.z : -180.0f));
fbxModel.rotationMax = glm::radians(glm::vec3(rotationMaxX ? rotationMax.x : 180.0f,
rotationMaxY ? rotationMax.y : 180.0f, rotationMaxZ ? rotationMax.z : 180.0f));
fbxModel.hasGeometricOffset = hasGeometricOffset;
fbxModel.geometricTranslation = geometricTranslation;
fbxModel.geometricRotation = glm::quat(glm::radians(geometricRotation));
fbxModel.geometricScaling = geometricScaling;
fbxModels.insert(getID(object.properties), fbxModel);
} else if (object.name == "Texture") {
TextureParam tex;
foreach (const FBXNode& subobject, object.children) {
const int RELATIVE_FILENAME_MIN_SIZE = 1;
const int TEXTURE_NAME_MIN_SIZE = 1;
const int TEXTURE_ALPHA_SOURCE_MIN_SIZE = 1;
const int MODEL_UV_TRANSLATION_MIN_SIZE = 2;
const int MODEL_UV_SCALING_MIN_SIZE = 2;
const int CROPPING_MIN_SIZE = 4;
if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) {
hifi::ByteArray filename = subobject.properties.at(0).toByteArray();
hifi::ByteArray filepath = filename.replace('\\', '/');
filename = fileOnUrl(filepath, url);
_textureFilepaths.insert(getID(object.properties), filepath);
_textureFilenames.insert(getID(object.properties), filename);
} else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) {
// trim the name from the timestamp
QString name = QString(subobject.properties.at(0).toByteArray());
name = name.left(name.indexOf('['));
_textureNames.insert(getID(object.properties), name);
} else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) {
tex.assign<uint8_t>(tex.alphaSource, subobject.properties.at(0).value<int>());
} else if (subobject.name == "ModelUVTranslation" && subobject.properties.length() >= MODEL_UV_TRANSLATION_MIN_SIZE) {
auto newTranslation = glm::vec3(subobject.properties.at(0).value<double>(), subobject.properties.at(1).value<double>(), 0.0);
tex.assign(tex.translation, tex.translation + newTranslation);
} else if (subobject.name == "ModelUVScaling" && subobject.properties.length() >= MODEL_UV_SCALING_MIN_SIZE) {
auto newScaling = glm::vec3(subobject.properties.at(0).value<double>(), subobject.properties.at(1).value<double>(), 1.0);
if (newScaling.x == 0.0f) {
newScaling.x = 1.0f;
}
if (newScaling.y == 0.0f) {
newScaling.y = 1.0f;
}
tex.assign(tex.scaling, tex.scaling * newScaling);
} else if (subobject.name == "Cropping" && subobject.properties.length() >= CROPPING_MIN_SIZE) {
tex.assign(tex.cropping, glm::vec4(subobject.properties.at(0).value<int>(),
subobject.properties.at(1).value<int>(),
subobject.properties.at(2).value<int>(),
subobject.properties.at(3).value<int>()));
} else if (subobject.name == "Properties70") {
hifi::ByteArray propertyName;
int index;
propertyName = "P";
index = 4;
foreach (const FBXNode& property, subobject.children) {
static const QVariant UV_SET = hifi::ByteArray("UVSet");
static const QVariant CURRENT_TEXTURE_BLEND_MODE = hifi::ByteArray("CurrentTextureBlendMode");
static const QVariant USE_MATERIAL = hifi::ByteArray("UseMaterial");
static const QVariant TRANSLATION = hifi::ByteArray("Translation");
static const QVariant ROTATION = hifi::ByteArray("Rotation");
static const QVariant SCALING = hifi::ByteArray("Scaling");
if (property.name == propertyName) {
QString v = property.properties.at(0).toString();
if (property.properties.at(0) == UV_SET) {
std::string uvName = property.properties.at(index).toString().toStdString();
tex.assign(tex.UVSet, property.properties.at(index).toString());
} else if (property.properties.at(0) == CURRENT_TEXTURE_BLEND_MODE) {
tex.assign<uint8_t>(tex.currentTextureBlendMode, property.properties.at(index).value<int>());
} else if (property.properties.at(0) == USE_MATERIAL) {
tex.assign<bool>(tex.useMaterial, property.properties.at(index).value<int>());
} else if (property.properties.at(0) == TRANSLATION) {
tex.assign(tex.translation, tex.translation + getVec3(property.properties, index));
} else if (property.properties.at(0) == ROTATION) {
tex.assign(tex.rotation, getVec3(property.properties, index));
} else if (property.properties.at(0) == SCALING) {
auto newScaling = getVec3(property.properties, index);
if (newScaling.x == 0.0f) {
newScaling.x = 1.0f;
}
if (newScaling.y == 0.0f) {
newScaling.y = 1.0f;
}
if (newScaling.z == 0.0f) {
newScaling.z = 1.0f;
}
tex.assign(tex.scaling, tex.scaling * newScaling);
}
#if defined(DEBUG_FBXSERIALIZER)
else {
QString propName = v;
unknown++;
}
#endif
}
}
}
#if defined(DEBUG_FBXSERIALIZER)
else {
if (subobject.name == "Type") {
} else if (subobject.name == "Version") {
} else if (subobject.name == "FileName") {
} else if (subobject.name == "Media") {
} else {
QString subname = subobject.name.data();
unknown++;
}
}
#endif
}
if (!tex.isDefault) {
_textureParams.insert(getID(object.properties), tex);
}
} else if (object.name == "Video") {
hifi::ByteArray filepath;
hifi::ByteArray content;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == "RelativeFilename") {
filepath = subobject.properties.at(0).toByteArray();
filepath = filepath.replace('\\', '/');
} else if (subobject.name == "Content" && !subobject.properties.isEmpty()) {
content = subobject.properties.at(0).toByteArray();
}
}
if (!content.isEmpty()) {
_textureContent.insert(filepath, content);
}
} else if (object.name == "Material") {
HFMMaterial material;
MaterialParam materialParam;
material.name = getMaterialName(object.properties);
foreach (const FBXNode& subobject, object.children) {
bool properties = false;
hifi::ByteArray propertyName;
int index;
if (subobject.name == "Properties60") {
properties = true;
propertyName = "Property";
index = 3;
} else if (subobject.name == "Properties70") {
properties = true;
propertyName = "P";
index = 4;
} else if (subobject.name == "ShadingModel") {
material.shadingModel = subobject.properties.at(0).toString();
}
if (properties) {
std::vector<std::string> unknowns;
static const QVariant DIFFUSE_COLOR = hifi::ByteArray("DiffuseColor");
static const QVariant DIFFUSE_FACTOR = hifi::ByteArray("DiffuseFactor");
static const QVariant DIFFUSE = hifi::ByteArray("Diffuse");
static const QVariant SPECULAR_COLOR = hifi::ByteArray("SpecularColor");
static const QVariant SPECULAR_FACTOR = hifi::ByteArray("SpecularFactor");
static const QVariant SPECULAR = hifi::ByteArray("Specular");
static const QVariant EMISSIVE_COLOR = hifi::ByteArray("EmissiveColor");
static const QVariant EMISSIVE_FACTOR = hifi::ByteArray("EmissiveFactor");
static const QVariant EMISSIVE = hifi::ByteArray("Emissive");
static const QVariant AMBIENT_FACTOR = hifi::ByteArray("AmbientFactor");
static const QVariant SHININESS = hifi::ByteArray("Shininess");
static const QVariant OPACITY = hifi::ByteArray("Opacity");
static const QVariant MAYA_USE_NORMAL_MAP = hifi::ByteArray("Maya|use_normal_map");
static const QVariant MAYA_BASE_COLOR = hifi::ByteArray("Maya|base_color");
static const QVariant MAYA_USE_COLOR_MAP = hifi::ByteArray("Maya|use_color_map");
static const QVariant MAYA_ROUGHNESS = hifi::ByteArray("Maya|roughness");
static const QVariant MAYA_USE_ROUGHNESS_MAP = hifi::ByteArray("Maya|use_roughness_map");
static const QVariant MAYA_METALLIC = hifi::ByteArray("Maya|metallic");
static const QVariant MAYA_USE_METALLIC_MAP = hifi::ByteArray("Maya|use_metallic_map");
static const QVariant MAYA_EMISSIVE = hifi::ByteArray("Maya|emissive");
static const QVariant MAYA_EMISSIVE_INTENSITY = hifi::ByteArray("Maya|emissive_intensity");
static const QVariant MAYA_USE_EMISSIVE_MAP = hifi::ByteArray("Maya|use_emissive_map");
static const QVariant MAYA_USE_AO_MAP = hifi::ByteArray("Maya|use_ao_map");
static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale");
static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset");
static const int MAYA_UV_OFFSET_PROPERTY_LENGTH = 6;
static const int MAYA_UV_SCALE_PROPERTY_LENGTH = 6;
foreach(const FBXNode& property, subobject.children) {
if (property.name == propertyName) {
if (property.properties.at(0) == DIFFUSE_COLOR) {
material.diffuseColor = getVec3(property.properties, index);
} else if (property.properties.at(0) == DIFFUSE_FACTOR) {
material.diffuseFactor = property.properties.at(index).value<double>();
} else if (property.properties.at(0) == DIFFUSE) {
// NOTE: this is uneeded but keep it for now for debug
// material.diffuseColor = getVec3(property.properties, index);
// material.diffuseFactor = 1.0;
} else if (property.properties.at(0) == SPECULAR_COLOR) {
material.specularColor = getVec3(property.properties, index);
} else if (property.properties.at(0) == SPECULAR_FACTOR) {
material.specularFactor = property.properties.at(index).value<double>();
} else if (property.properties.at(0) == SPECULAR) {
// NOTE: this is uneeded but keep it for now for debug
// material.specularColor = getVec3(property.properties, index);
// material.specularFactor = 1.0;
} else if (property.properties.at(0) == EMISSIVE_COLOR) {
material.emissiveColor = getVec3(property.properties, index);
} else if (property.properties.at(0) == EMISSIVE_FACTOR) {
material.emissiveFactor = property.properties.at(index).value<double>();
} else if (property.properties.at(0) == EMISSIVE) {
// NOTE: this is uneeded but keep it for now for debug
// material.emissiveColor = getVec3(property.properties, index);
// material.emissiveFactor = 1.0;
} else if (property.properties.at(0) == AMBIENT_FACTOR) {
material.ambientFactor = property.properties.at(index).value<double>();
// Detected just for BLender AO vs lightmap
} else if (property.properties.at(0) == SHININESS) {
material.shininess = property.properties.at(index).value<double>();
} else if (property.properties.at(0) == OPACITY) {
material.opacity = property.properties.at(index).value<double>();
}
// Sting Ray Material Properties!!!!
else if (property.properties.at(0) == MAYA_USE_NORMAL_MAP) {
material.isPBSMaterial = true;
material.useNormalMap = (bool)property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_BASE_COLOR) {
material.isPBSMaterial = true;
material.diffuseColor = getVec3(property.properties, index);
} else if (property.properties.at(0) == MAYA_USE_COLOR_MAP) {
material.isPBSMaterial = true;
material.useAlbedoMap = (bool) property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_ROUGHNESS) {
material.isPBSMaterial = true;
material.roughness = property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_USE_ROUGHNESS_MAP) {
material.isPBSMaterial = true;
material.useRoughnessMap = (bool)property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_METALLIC) {
material.isPBSMaterial = true;
material.metallic = property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_USE_METALLIC_MAP) {
material.isPBSMaterial = true;
material.useMetallicMap = (bool)property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_EMISSIVE) {
material.isPBSMaterial = true;
material.emissiveColor = getVec3(property.properties, index);
} else if (property.properties.at(0) == MAYA_EMISSIVE_INTENSITY) {
material.isPBSMaterial = true;
material.emissiveIntensity = property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_USE_EMISSIVE_MAP) {
material.isPBSMaterial = true;
material.useEmissiveMap = (bool)property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_USE_AO_MAP) {
material.isPBSMaterial = true;
material.useOcclusionMap = (bool)property.properties.at(index).value<double>();
} else if (property.properties.at(0) == MAYA_UV_SCALE) {
if (property.properties.size() == MAYA_UV_SCALE_PROPERTY_LENGTH) {
// properties: { "Maya|uv_scale", "Vector2D", "Vector2", nothing, double, double }
glm::vec3 scale = glm::vec3(property.properties.at(4).value<double>(), property.properties.at(5).value<double>(), 1.0);
if (scale.x == 0.0f) {
scale.x = 1.0f;
}
if (scale.y == 0.0f) {
scale.y = 1.0f;
}
if (scale.z == 0.0f) {
scale.z = 1.0f;
}
materialParam.scaling *= scale;
}
} else if (property.properties.at(0) == MAYA_UV_OFFSET) {
if (property.properties.size() == MAYA_UV_OFFSET_PROPERTY_LENGTH) {
// properties: { "Maya|uv_offset", "Vector2D", "Vector2", nothing, double, double }
glm::vec3 translation = glm::vec3(property.properties.at(4).value<double>(), property.properties.at(5).value<double>(), 1.0);
materialParam.translation += translation;
}
} else {
const QString propname = property.properties.at(0).toString();
unknowns.push_back(propname.toStdString());
}
}
}
}
#if defined(DEBUG_FBXSERIALIZER)
else {
QString propname = subobject.name.data();
int unknown = 0;
if ( (propname == "Version")
||(propname == "Multilayer")) {
} else {
unknown++;
}
}
#endif
}
material.materialID = getID(object.properties);
_hfmMaterials.insert(material.materialID, material);
_materialParams.insert(material.materialID, materialParam);
} else if (object.name == "NodeAttribute") {
#if defined(DEBUG_FBXSERIALIZER)
std::vector<QString> properties;
foreach(const QVariant& v, object.properties) {
properties.push_back(v.toString());
}
#endif
QString attribID = getID(object.properties);
QString attributetype;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == "TypeFlags") {
typeFlags.insert(getID(object.properties), subobject.properties.at(0).toString());
attributetype = subobject.properties.at(0).toString();
}
}
if (!attributetype.isEmpty()) {
if (attributetype == "Light") {
HFMLight light = extractLight(object);
lights[attribID] = light;
}
}
} else if (object.name == "Deformer") {
if (object.properties.last() == "Cluster") {
Cluster cluster;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == "Indexes") {
cluster.indices = getIntVector(subobject);
} else if (subobject.name == "Weights") {
cluster.weights = getDoubleVector(subobject);
} else if (subobject.name == "TransformLink") {
QVector<double> values = getDoubleVector(subobject);
cluster.transformLink = createMat4(values);
}
}
// skip empty fbxClusters
if (cluster.indices.size() > 0 && cluster.weights.size() > 0) {
fbxClusters.insert(getID(object.properties), cluster);
}
} else if (object.properties.last() == "BlendShapeChannel") {
hifi::ByteArray name = object.properties.at(1).toByteArray();
name = name.left(name.indexOf('\0'));
if (!blendshapeIndices.contains(name)) {
// try everything after the dot
name = name.mid(name.lastIndexOf('.') + 1);
}
QString id = getID(object.properties);
hfmModel.blendshapeChannelNames << name;
foreach (const WeightedIndex& index, blendshapeIndices.values(name)) {
blendshapeChannelIndices.insert(id, index);
}
}
} else if (object.name == "AnimationCurve") {
AnimationCurve curve;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == "KeyValueFloat") {
curve.values = getFloatVector(subobject);
}
}
animationCurves.insert(getID(object.properties), curve);
}
#if defined(DEBUG_FBXSERIALIZER)
else {
QString objectname = object.name.data();
if ( objectname == "Pose"
|| objectname == "AnimationStack"
|| objectname == "AnimationLayer"
|| objectname == "AnimationCurveNode") {
} else {
unknown++;
}
}
#endif
}
} else if (child.name == "Connections") {
static const QVariant OO = hifi::ByteArray("OO");
static const QVariant OP = hifi::ByteArray("OP");
foreach (const FBXNode& connection, child.children) {
if (connection.name == "C" || connection.name == "Connect") {
if (connection.properties.at(0) == OO) {
QString childID = getID(connection.properties, 1);
QString parentID = getID(connection.properties, 2);
ooChildToParent.insert(childID, parentID);
if (!hifiGlobalNodeID.isEmpty() && (parentID == hifiGlobalNodeID)) {
std::map< QString, HFMLight >::iterator lightIt = lights.find(childID);
if (lightIt != lights.end()) {
_lightmapLevel = (*lightIt).second.intensity;
if (_lightmapLevel <= 0.0f) {
_loadLightmaps = false;
}
_lightmapOffset = glm::clamp((*lightIt).second.color.x, 0.f, 1.f);
}
}
} else if (connection.properties.at(0) == OP) {
int counter = 0;
hifi::ByteArray type = connection.properties.at(3).toByteArray().toLower();
if (type.contains("DiffuseFactor")) {
diffuseFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if ((type.contains("diffuse") && !type.contains("tex_global_diffuse"))) {
diffuseTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("tex_color_map")) {
diffuseTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("transparentcolor")) { // Maya way of passing TransparentMap
transparentTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("transparencyfactor")) { // Blender way of passing TransparentMap
transparentTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("bump")) {
bumpTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("normal")) {
normalTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("tex_normal_map")) {
normalTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if ((type.contains("specular") && !type.contains("tex_global_specular")) || type.contains("reflection")) {
specularTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("tex_metallic_map")) {
metallicTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("shininess")) {
shininessTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("tex_roughness_map")) {
roughnessTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("emissive")) {
emissiveTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("tex_emissive_map")) {
emissiveTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("ambientcolor")) {
ambientTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("ambientfactor")) {
ambientFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type.contains("tex_ao_map")) {
occlusionTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type == "lcl rotation") {
localRotations.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type == "lcl translation") {
localTranslations.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type == "d|x") {
xComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type == "d|y") {
yComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if (type == "d|z") {
zComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else {
QString typenam = type.data();
counter++;
}
}
if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") {
// don't assign the new parent
qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2);
_connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else {
_connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2));
_connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1));
}
}
}
}
#if defined(DEBUG_FBXSERIALIZER)
else {
QString objectname = child.name.data();
if ( objectname == "Pose"
|| objectname == "CreationTime"
|| objectname == "FileId"
|| objectname == "Creator"
|| objectname == "Documents"
|| objectname == "References"
|| objectname == "Definitions"
|| objectname == "Takes"
|| objectname == "AnimationStack"
|| objectname == "AnimationLayer"
|| objectname == "AnimationCurveNode") {
} else {
unknown++;
}
}
#endif
}
// TODO: check if is code is needed
if (!lights.empty()) {
if (hifiGlobalNodeID.isEmpty()) {
auto light = lights.begin();
_lightmapLevel = (*light).second.intensity;
}
}
// assign the blendshapes to their corresponding meshes
foreach (const ExtractedBlendshape& blendshape, blendshapes) {
QString blendshapeChannelID = _connectionParentMap.value(blendshape.id);
QString blendshapeID = _connectionParentMap.value(blendshapeChannelID);
QString meshID = _connectionParentMap.value(blendshapeID);
addBlendshapes(blendshape, blendshapeChannelIndices.values(blendshapeChannelID), meshes[meshID]);
}
// get offset transform from mapping
float offsetScale = mapping.value("scale", 1.0f).toFloat() * unitScaleFactor * METERS_PER_CENTIMETER;
glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(),
mapping.value("ry").toFloat(), mapping.value("rz").toFloat())));
hfmModel.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(),
mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) *
glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale));
// get the list of models in depth-first traversal order
QVector<QString> modelIDs;
QSet<QString> remainingFBXModels;
for (QHash<QString, FBXModel>::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) {
// models with fbxClusters must be parented to the cluster top
// Unless the model is a root node.
bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key()));
if (!isARootNode) {
foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) {
foreach(const QString& clusterID, _connectionChildMap.values(deformerID)) {
if (!fbxClusters.contains(clusterID)) {
continue;
}
QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url);
_connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key());
_connectionParentMap.insert(fbxModel.key(), topID);
goto outerBreak;
}
}
outerBreak: ;
}
// make sure the parent is in the child map
QString parent = _connectionParentMap.value(fbxModel.key());
if (!_connectionChildMap.contains(parent, fbxModel.key())) {
_connectionChildMap.insert(parent, fbxModel.key());
}
remainingFBXModels.insert(fbxModel.key());
}
while (!remainingFBXModels.isEmpty()) {
QString first = *remainingFBXModels.constBegin();
foreach (const QString& id, remainingFBXModels) {
if (id < first) {
first = id;
}
}
QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url);
appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true);
}
// figure the number of animation frames from the curves
int frameCount = 1;
foreach (const AnimationCurve& curve, animationCurves) {
frameCount = qMax(frameCount, curve.values.size());
}
for (int i = 0; i < frameCount; i++) {
HFMAnimationFrame frame;
frame.rotations.resize(modelIDs.size());
frame.translations.resize(modelIDs.size());
hfmModel.animationFrames.append(frame);
}
// convert the models to joints
hfmModel.hasSkeletonJoints = false;
bool needMixamoHack = hfmModel.applicationName == "mixamo.com";
for (const QString& modelID : modelIDs) {
const FBXModel& fbxModel = fbxModels[modelID];
HFMJoint joint;
joint.parentIndex = fbxModel.parentIndex;
uint32_t jointIndex = (uint32_t)hfmModel.joints.size();
joint.translation = fbxModel.translation; // these are usually in centimeters
joint.preTransform = fbxModel.preTransform;
joint.preRotation = fbxModel.preRotation;
joint.rotation = fbxModel.rotation;
joint.postRotation = fbxModel.postRotation;
joint.postTransform = fbxModel.postTransform;
joint.rotationMin = fbxModel.rotationMin;
joint.rotationMax = fbxModel.rotationMax;
joint.hasGeometricOffset = fbxModel.hasGeometricOffset;
joint.geometricTranslation = fbxModel.geometricTranslation;
joint.geometricRotation = fbxModel.geometricRotation;
joint.geometricScaling = fbxModel.geometricScaling;
joint.isSkeletonJoint = fbxModel.isLimbNode;
hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint);
if (applyUpAxisZRotation && joint.parentIndex == -1) {
joint.rotation *= upAxisZRotation;
joint.translation = upAxisZRotation * joint.translation;
}
glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation;
if (joint.parentIndex == -1) {
joint.transform = hfmModel.offset * glm::translate(joint.translation) * joint.preTransform *
glm::mat4_cast(combinedRotation) * joint.postTransform;
joint.inverseDefaultRotation = glm::inverse(combinedRotation);
joint.distanceToParent = 0.0f;
} else {
const HFMJoint& parentJoint = hfmModel.joints.at(joint.parentIndex);
joint.transform = parentJoint.transform * glm::translate(joint.translation) *
joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform;
joint.inverseDefaultRotation = glm::inverse(combinedRotation) * parentJoint.inverseDefaultRotation;
joint.distanceToParent = glm::distance(extractTranslation(parentJoint.transform),
extractTranslation(joint.transform));
}
joint.inverseBindRotation = joint.inverseDefaultRotation;
joint.name = fbxModel.name;
joint.bindTransformFoundInCluster = false;
QString rotationID = localRotations.value(modelID);
AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID));
AnimationCurve yRotCurve = animationCurves.value(yComponents.value(rotationID));
AnimationCurve zRotCurve = animationCurves.value(zComponents.value(rotationID));
QString translationID = localTranslations.value(modelID);
AnimationCurve xPosCurve = animationCurves.value(xComponents.value(translationID));
AnimationCurve yPosCurve = animationCurves.value(yComponents.value(translationID));
AnimationCurve zPosCurve = animationCurves.value(zComponents.value(translationID));
glm::vec3 defaultRotValues = glm::degrees(safeEulerAngles(joint.rotation));
glm::vec3 defaultPosValues = joint.translation;
for (int i = 0; i < frameCount; i++) {
hfmModel.animationFrames[i].rotations[jointIndex] = glm::quat(glm::radians(glm::vec3(
xRotCurve.values.isEmpty() ? defaultRotValues.x : xRotCurve.values.at(i % xRotCurve.values.size()),
yRotCurve.values.isEmpty() ? defaultRotValues.y : yRotCurve.values.at(i % yRotCurve.values.size()),
zRotCurve.values.isEmpty() ? defaultRotValues.z : zRotCurve.values.at(i % zRotCurve.values.size()))));
hfmModel.animationFrames[i].translations[jointIndex] = glm::vec3(
xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()),
yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()),
zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size()));
if ((fbxVersionNumber < 7500) && (i == 0)) {
joint.translation = hfmModel.animationFrames[i].translations[jointIndex];
joint.rotation = hfmModel.animationFrames[i].rotations[jointIndex];
}
}
// Now that we've initialized the joint, we can define the transform
// modelIDs is ordered from parent to children, so we can safely get parent transforms from earlier joints as we iterate
joint.localTransform = glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation * joint.postRotation) * joint.postTransform;
joint.globalTransform = joint.localTransform;
if (joint.parentIndex != -1 && joint.parentIndex < (int)jointIndex && !needMixamoHack) {
hfm::Joint& parentJoint = hfmModel.joints[joint.parentIndex];
// SG Change: i think this not correct and the [parent]*[local] is the correct answer here
//joint.globalTransform = joint.globalTransform * parentJoint.globalTransform;
joint.globalTransform = parentJoint.globalTransform * joint.localTransform;
if (parentJoint.hasGeometricOffset) {
// Per the FBX standard, geometric offset should not propagate to children.
// However, we must be careful when modifying the behavior of FBXSerializer.
// So, we leave this here, as a breakpoint for debugging, or stub for implementation.
// qCDebug(modelformat) << "Geometric offset encountered on non-leaf node. jointIndex: " << jointIndex << ", modelURL: " << url;
// glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(parentJoint.geometricScaling, parentJoint.geometricRotation, parentJoint.geometricTranslation);
// globalTransform = globalTransform * glm::inverse(geometricOffset);
}
}
if (joint.hasGeometricOffset) {
glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(joint.geometricScaling, joint.geometricRotation, joint.geometricTranslation);
joint.globalTransform = joint.globalTransform * geometricOffset;
}
// TODO: Remove these lines, just here to make sure we are not breaking the transform computation
// QString modelID = fbxModels.contains(it.key()) ? it.key() : _connectionParentMap.value(it.key());
glm::mat4 anotherModelTransform = getGlobalTransform(_connectionParentMap, fbxModels, modelID, hfmModel.applicationName == "mixamo.com", url);
auto col0 = (glm::epsilonNotEqual(anotherModelTransform[0], joint.globalTransform[0], 0.001f));
auto col1 = (glm::epsilonNotEqual(anotherModelTransform[1], joint.globalTransform[1], 0.001f));
auto col2 = (glm::epsilonNotEqual(anotherModelTransform[2], joint.globalTransform[2], 0.001f));
auto col3 = (glm::epsilonNotEqual(anotherModelTransform[3], joint.globalTransform[3], 0.001f));
if ( glm::any(col0)
|| glm::any(col1)
|| glm::any(col2)
|| glm::any(col3)) {
anotherModelTransform = getGlobalTransform(_connectionParentMap, fbxModels, modelID, hfmModel.applicationName == "mixamo.com", url);
// joint.globalTransform = anotherModelTransform;
}
hfmModel.joints.push_back(joint);
}
hfmModel.bindExtents.reset();
hfmModel.meshExtents.reset();
// Create the Material Library
consolidateHFMMaterials();
// We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image
// Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key
#if 0
// HACK: until we get proper LOD management we're going to cap model textures
// according to how many unique textures the model uses:
// 1 - 8 textures --> 2048
// 8 - 32 textures --> 1024
// 33 - 128 textures --> 512
// etc...
QSet<QString> uniqueTextures;
for (auto& material : _hfmMaterials) {
material.getTextureNames(uniqueTextures);
}
int numTextures = uniqueTextures.size();
const int MAX_NUM_TEXTURES_AT_MAX_RESOLUTION = 8;
int maxWidth = sqrt(MAX_NUM_PIXELS_FOR_FBX_TEXTURE);
if (numTextures > MAX_NUM_TEXTURES_AT_MAX_RESOLUTION) {
int numTextureThreshold = MAX_NUM_TEXTURES_AT_MAX_RESOLUTION;
const int MIN_MIP_TEXTURE_WIDTH = 64;
do {
maxWidth /= 2;
numTextureThreshold *= 4;
} while (numTextureThreshold < numTextures && maxWidth > MIN_MIP_TEXTURE_WIDTH);
qCDebug(modelformat) << "Capped square texture width =" << maxWidth << "for model" << url << "with" << numTextures << "textures";
for (auto& material : _hfmMaterials) {
material.setMaxNumPixelsPerTexture(maxWidth * maxWidth);
}
}
#endif
std::unordered_map<std::string, uint32_t> materialNameToID;
for (auto materialIt = _hfmMaterials.cbegin(); materialIt != _hfmMaterials.cend(); ++materialIt) {
materialNameToID[materialIt.key().toStdString()] = (uint32_t)hfmModel.materials.size();
hfmModel.materials.push_back(materialIt.value());
}
// see if any materials have texture children
bool materialsHaveTextures = checkMaterialsHaveTextures(_hfmMaterials, _textureFilenames, _connectionChildMap);
for (QMap<QString, ExtractedMesh>::iterator it = meshes.begin(); it != meshes.end(); it++) {
const QString& meshID = it.key();
const ExtractedMesh& extracted = it.value();
const auto& partMaterialTextures = extracted.partMaterialTextures;
uint32_t meshIndex = (uint32_t)hfmModel.meshes.size();
meshIDsToMeshIndices.insert(it.key(), meshIndex);
hfmModel.meshes.push_back(extracted.mesh);
hfm::Mesh& mesh = hfmModel.meshes.back();
std::vector<QString> instanceModelIDs = getModelIDsForMeshID(meshID, fbxModels, _connectionParentMap);
// meshShapes will be added to hfmModel at the very end
std::vector<hfm::Shape> meshShapes;
meshShapes.reserve(instanceModelIDs.size() * mesh.parts.size());
if (instanceModelIDs.size() > 1) {
qCDebug(modelformat) << "Mesh " << meshID << " made of " << mesh.parts.size() << " parts is instanced " << instanceModelIDs.size() << " times!!!";
}
if (mesh.parts.size() < 1) {
qCDebug(modelformat) << "Mesh " << meshID << " made of " << mesh.parts.size() << " parts !!!!! ";
}
for (const QString& modelID : instanceModelIDs) {
// The transform node has the same indexing order as the joints
int indexOfModelID = modelIDs.indexOf(modelID);
if (indexOfModelID == -1) {
qCDebug(modelformat) << "Model not in model list: " << modelID;
}
const uint32_t transformIndex = (indexOfModelID == -1) ? 0 : (uint32_t)indexOfModelID;
// accumulate local transforms
glm::mat4 globalTransform = hfmModel.joints[transformIndex].globalTransform;
// compute the mesh extents from the transformed vertices
for (const glm::vec3& vertex : mesh.vertices) {
glm::vec3 transformedVertex = glm::vec3(globalTransform * glm::vec4(vertex, 1.0f));
hfmModel.meshExtents.minimum = glm::min(hfmModel.meshExtents.minimum, transformedVertex);
hfmModel.meshExtents.maximum = glm::max(hfmModel.meshExtents.maximum, transformedVertex);
}
// partShapes will be added to meshShapes at the very end
std::vector<hfm::Shape> partShapes { mesh.parts.size() };
for (uint32_t i = 0; i < (uint32_t)partShapes.size(); ++i) {
hfm::Shape& shape = partShapes[i];
shape.mesh = meshIndex;
shape.meshPart = i;
shape.joint = transformIndex;
auto matName = mesh.parts[i].materialID;
auto materialIt = materialNameToID.find(matName.toStdString());
if (materialIt != materialNameToID.end()) {
shape.material = materialIt->second;
} else {
qCDebug(modelformat) << "Unknown material ? " << matName;
}
shape.transformedExtents.reset();
// compute the shape extents from the transformed vertices
for (const glm::vec3& vertex : mesh.vertices) {
glm::vec3 transformedVertex = glm::vec3(globalTransform * glm::vec4(vertex, 1.0f));
shape.transformedExtents.minimum = glm::min(shape.transformedExtents.minimum, transformedVertex);
shape.transformedExtents.maximum = glm::max(shape.transformedExtents.maximum, transformedVertex);
}
}
// For FBX_DRACO_MESH_VERSION < 2, or unbaked models, get materials from the partMaterialTextures
if (!partMaterialTextures.empty()) {
int materialIndex = 0;
int textureIndex = 0;
QList<QString> children = _connectionChildMap.values(modelID);
for (int i = children.size() - 1; i >= 0; i--) {
const QString& childID = children.at(i);
if (_hfmMaterials.contains(childID)) {
// the pure material associated with this part
const HFMMaterial& material = _hfmMaterials.value(childID);
for (int j = 0; j < partMaterialTextures.size(); j++) {
if (partMaterialTextures.at(j).first == materialIndex) {
hfm::Shape& shape = partShapes[j];
shape.material = materialNameToID[material.materialID.toStdString()];
}
}
materialIndex++;
} else if (_textureFilenames.contains(childID)) {
// NOTE (Sabrina 2019/01/11): getTextures now takes in the materialID as a second parameter, because FBX material nodes can sometimes have uv transform information (ex: "Maya|uv_scale")
// I'm leaving the second parameter blank right now as this code may never be used.
HFMTexture texture = getTexture(childID, "");
for (int j = 0; j < partMaterialTextures.size(); j++) {
int partTexture = partMaterialTextures.at(j).second;
if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) {
// TODO: DO something here that replaces this legacy code
// Maybe create a material just for this part with the correct textures?
// material.albedoTexture = texture;
// partShapes[j].material = materialIndex;
}
}
textureIndex++;
}
}
}
// For baked models with FBX_DRACO_MESH_VERSION >= 2, get materials from extracted.materialIDPerMeshPart
if (!extracted.materialIDPerMeshPart.empty()) {
/* if (partShapes.size() == extracted.materialIDPerMeshPart.size()) {
for (uint32_t i = 0; i < (uint32_t)extracted.materialIDPerMeshPart.size(); ++i) {
hfm::Shape& shape = partShapes[i];
const std::string& materialID = extracted.materialIDPerMeshPart[i];
auto materialIt = materialNameToID.find(materialID);
if (materialIt != materialNameToID.end()) {
shape.material = materialIt->second;
}
}
} else {
for (int p = 0; p < mesh.parts.size(); p++) {
qCDebug(modelformat) << "mesh.parts[" << p <<"] is " << mesh.parts[p].materialID;
}
qCDebug(modelformat) << "partShapes is not the same size as materialIDPerMeshPart ?";
}*/
}
// find the clusters with which the mesh is associated
QVector<QString> clusterIDs;
for (const QString& childID : _connectionChildMap.values(meshID)) {
for (const QString& clusterID : _connectionChildMap.values(childID)) {
if (!fbxClusters.contains(clusterID)) {
continue;
}
clusterIDs.append(clusterID);
}
}
// whether we're skinned depends on how many clusters are attached
if (clusterIDs.size() > 0) {
hfm::SkinDeformer skinDeformer;
auto& clusters = skinDeformer.clusters;
for (const auto& clusterID : clusterIDs) {
HFMCluster hfmCluster;
const Cluster& fbxCluster = fbxClusters[clusterID];
// see http://stackoverflow.com/questions/13566608/loading-skinning-information-from-fbx for a discussion
// of skinning information in FBX
QString jointID = _connectionChildMap.value(clusterID);
int indexOfJointID = modelIDs.indexOf(jointID);
if (indexOfJointID == -1) {
qCDebug(modelformat) << "Joint not in model list: " << jointID;
hfmCluster.jointIndex = 0;
} else {
hfmCluster.jointIndex = (uint32_t)indexOfJointID;
}
hfmCluster.inverseBindMatrix = glm::inverse(fbxCluster.transformLink) * globalTransform;
// slam bottom row to (0, 0, 0, 1), we KNOW this is not a perspective matrix and
// sometimes floating point fuzz can be introduced after the inverse.
hfmCluster.inverseBindMatrix[0][3] = 0.0f;
hfmCluster.inverseBindMatrix[1][3] = 0.0f;
hfmCluster.inverseBindMatrix[2][3] = 0.0f;
hfmCluster.inverseBindMatrix[3][3] = 1.0f;
hfmCluster.inverseBindTransform = Transform(hfmCluster.inverseBindMatrix);
clusters.push_back(hfmCluster);
// override the bind rotation with the transform link
HFMJoint& joint = hfmModel.joints[hfmCluster.jointIndex];
joint.inverseBindRotation = glm::inverse(extractRotation(fbxCluster.transformLink));
joint.bindTransform = fbxCluster.transformLink;
joint.bindTransformFoundInCluster = true;
// update the bind pose extents
glm::vec3 bindTranslation = extractTranslation(hfmModel.offset * joint.bindTransform);
hfmModel.bindExtents.addPoint(bindTranslation);
}
// the last cluster is the root cluster
HFMCluster cluster;
cluster.jointIndex = transformIndex;
clusters.push_back(cluster);
std::vector<hfm::SkinCluster> skinClusters;
// Skinned mesh instances have an hfm::SkinDeformer
skinDeformer.skinClusterIndices.reserve(clusterIDs.size());
for (const auto& clusterID : clusterIDs) {
const Cluster& fbxCluster = fbxClusters[clusterID];
skinDeformer.skinClusterIndices.emplace_back();
skinClusters.emplace_back();
hfm::SkinCluster& skinCluster = skinClusters.back();
size_t indexWeightPairs = (size_t)std::min(fbxCluster.indices.size(), fbxCluster.weights.size());
skinCluster.indices.reserve(indexWeightPairs);
skinCluster.weights.reserve(indexWeightPairs);
for (int j = 0; j < fbxCluster.indices.size(); j++) {
int oldIndex = fbxCluster.indices.at(j);
float weight = fbxCluster.weights.at(j);
for (QMultiHash<int, int>::const_iterator it = extracted.newIndices.constFind(oldIndex);
it != extracted.newIndices.end() && it.key() == oldIndex; it++) {
int newIndex = it.value();
skinCluster.indices.push_back(newIndex);
skinCluster.weights.push_back(weight);
}
}
}
// It seems odd that this mesh-related code should be inside of the for loop for instanced model IDs.
// However, in practice, skinned FBX models appear to not be instanced, as the skinning includes both the weights and joints.
{
hfm::ReweightedDeformers reweightedDeformers = hfm::getReweightedDeformers(mesh.vertices.size(), skinClusters);
if (reweightedDeformers.trimmedToMatch) {
qDebug(modelformat) << "FBXSerializer -- The number of indices and weights for a skinning deformer had different sizes and have been trimmed to match";
}
mesh.clusterIndices = std::move(reweightedDeformers.indices);
mesh.clusterWeights = std::move(reweightedDeformers.weights);
mesh.clusterWeightsPerVertex = reweightedDeformers.weightsPerVertex;
}
// Store the model's dynamic transform, and put its ID in the shapes
hfmModel.skinDeformers.push_back(skinDeformer);
uint32_t skinDeformerID = (uint32_t)(hfmModel.skinDeformers.size() - 1);
for (hfm::Shape& shape : partShapes) {
shape.skinDeformer = skinDeformerID;
}
} else {
// this is a no cluster mesh
HFMJoint& joint = hfmModel.joints[transformIndex];
// Apply geometric offset, if present, by transforming the vertices directly
if (joint.hasGeometricOffset) {
glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(joint.geometricScaling, joint.geometricRotation, joint.geometricTranslation);
for (int i = 0; i < mesh.vertices.size(); i++) {
mesh.vertices[i] = transformPoint(geometricOffset, mesh.vertices[i]);
}
}
}
// Store the parts for this mesh (or instance of this mesh, as the case may be)
meshShapes.insert(meshShapes.cend(), partShapes.cbegin(), partShapes.cend());
}
// Store the shapes for the mesh (or multiple instances of the mesh, as the case may be)
hfmModel.shapes.insert(hfmModel.shapes.cend(), meshShapes.cbegin(), meshShapes.cend());
}
// attempt to map any meshes to a named model
for (QHash<QString, int>::const_iterator m = meshIDsToMeshIndices.constBegin();
m != meshIDsToMeshIndices.constEnd(); m++) {
const QString& meshID = m.key();
int meshIndex = m.value();
if (ooChildToParent.contains(meshID)) {
const QString& modelID = ooChildToParent.value(meshID);
if (modelIDsToNames.contains(modelID)) {
const QString& modelName = modelIDsToNames.value(modelID);
hfmModel.meshIndicesToModelNames.insert(meshIndex, modelName);
}
}
}
if (applyUpAxisZRotation) {
hfmModelPtr->meshExtents.transform(glm::mat4_cast(upAxisZRotation));
hfmModelPtr->bindExtents.transform(glm::mat4_cast(upAxisZRotation));
for (auto& shape : hfmModelPtr->shapes) {
shape.transformedExtents.transform(glm::mat4_cast(upAxisZRotation));
}
for (auto& joint : hfmModelPtr->joints) {
joint.globalTransform = joint.globalTransform * glm::mat4_cast(upAxisZRotation);
}
}
return hfmModelPtr;
}
MediaType FBXSerializer::getMediaType() const {
MediaType mediaType("fbx");
mediaType.extensions.push_back("fbx");
mediaType.fileSignatures.emplace_back("Kaydara FBX Binary \x00", 0);
return mediaType;
}
std::unique_ptr<hfm::Serializer::Factory> FBXSerializer::getFactory() const {
return std::make_unique<hfm::Serializer::SimpleFactory<FBXSerializer>>();
}
HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) {
QBuffer buffer(const_cast<hifi::ByteArray*>(&data));
buffer.open(QIODevice::ReadOnly);
_rootNode = parseFBX(&buffer);
// FBXSerializer's mapping parameter supports the bool "deduplicateIndices," which is passed into FBXSerializer::extractMesh as "deduplicate"
return HFMModel::Pointer(extractHFMModel(mapping, url.toString()));
}