overte/libraries/fbx/src/FBXReader.cpp
2017-06-09 15:52:21 -07:00

1844 lines
92 KiB
C++

//
// FBXReader.cpp
// interface/src/renderer
//
// 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 <iostream>
#include <QBuffer>
#include <QDataStream>
#include <QIODevice>
#include <QStringList>
#include <QTextStream>
#include <QtDebug>
#include <QtEndian>
#include <QFileInfo>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/transform.hpp>
#include <FaceshiftConstants.h>
#include <GeometryUtil.h>
#include <GLMHelpers.h>
#include <NumericalConstants.h>
#include <OctalCode.h>
#include <gpu/Format.h>
#include <LogHandler.h>
#include "FBXReader.h"
#include "ModelFormatLogging.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_FBXREADER
using namespace std;
int FBXGeometryPointerMetaTypeId = qRegisterMetaType<FBXGeometry::Pointer>();
QStringList FBXGeometry::getJointNames() const {
QStringList names;
foreach (const FBXJoint& joint, joints) {
names.append(joint.name);
}
return names;
}
bool FBXGeometry::hasBlendedMeshes() const {
if (!meshes.isEmpty()) {
foreach (const FBXMesh& mesh, meshes) {
if (!mesh.blendshapes.isEmpty()) {
return true;
}
}
}
return false;
}
Extents FBXGeometry::getUnscaledMeshExtents() const {
const Extents& extents = meshExtents;
// even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which
// is captured in the offset matrix
glm::vec3 minimum = glm::vec3(offset * glm::vec4(extents.minimum, 1.0f));
glm::vec3 maximum = glm::vec3(offset * glm::vec4(extents.maximum, 1.0f));
Extents scaledExtents = { minimum, maximum };
return scaledExtents;
}
// TODO: Move to model::Mesh when Sam's ready
bool FBXGeometry::convexHullContains(const glm::vec3& point) const {
if (!getUnscaledMeshExtents().containsPoint(point)) {
return false;
}
auto checkEachPrimitive = [=](FBXMesh& mesh, QVector<int> indices, int primitiveSize) -> bool {
// Check whether the point is "behind" all the primitives.
int verticesSize = mesh.vertices.size();
for (int j = 0;
j < indices.size() - 2; // -2 in case the vertices aren't the right size -- we access j + 2 below
j += primitiveSize) {
if (indices[j] < verticesSize &&
indices[j + 1] < verticesSize &&
indices[j + 2] < verticesSize &&
!isPointBehindTrianglesPlane(point,
mesh.vertices[indices[j]],
mesh.vertices[indices[j + 1]],
mesh.vertices[indices[j + 2]])) {
// it's not behind at least one so we bail
return false;
}
}
return true;
};
// Check that the point is contained in at least one convex mesh.
for (auto mesh : meshes) {
bool insideMesh = true;
// To be considered inside a convex mesh,
// the point needs to be "behind" all the primitives respective planes.
for (auto part : mesh.parts) {
// run through all the triangles and quads
if (!checkEachPrimitive(mesh, part.triangleIndices, 3) ||
!checkEachPrimitive(mesh, part.quadIndices, 4)) {
// If not, the point is outside, bail for this mesh
insideMesh = false;
continue;
}
}
if (insideMesh) {
// It's inside this mesh, return true.
return true;
}
}
// It wasn't in any mesh, return false.
return false;
}
QString FBXGeometry::getModelNameOfMesh(int meshIndex) const {
if (meshIndicesToModelNames.contains(meshIndex)) {
return meshIndicesToModelNames.value(meshIndex);
}
return QString();
}
int fbxGeometryMetaTypeId = qRegisterMetaType<FBXGeometry>();
int fbxAnimationFrameMetaTypeId = qRegisterMetaType<FBXAnimationFrame>();
int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType<QVector<FBXAnimationFrame> >();
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;
}
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 getName(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 getID(const QVariantList& properties, int index = 0) {
return processID(properties.at(index).toString());
}
const char* HUMANIK_JOINTS[] = {
"RightHand",
"RightForeArm",
"RightArm",
"Head",
"LeftArm",
"LeftForeArm",
"LeftHand",
"Neck",
"Spine",
"Hips",
"RightUpLeg",
"LeftUpLeg",
"RightLeg",
"LeftLeg",
"RightFoot",
"LeftFoot",
""
};
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;
};
glm::mat4 getGlobalTransform(const QMultiMap<QString, QString>& _connectionParentMap,
const QHash<QString, FBXModel>& models, 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& model = models.value(nodeID);
globalTransform = glm::translate(model.translation) * model.preTransform * glm::mat4_cast(model.preRotation *
model.rotation * model.postRotation) * model.postTransform * globalTransform;
if (mixamoHack) {
// there's something weird about the models from Mixamo Fuse; they don't skin right with the full transform
return globalTransform;
}
QList<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 (models.contains(parentID)) {
nodeID = parentID;
break;
}
}
}
return globalTransform;
}
class ExtractedBlendshape {
public:
QString id;
FBXBlendshape blendshape;
};
void printNode(const FBXNode& node, int indentLevel) {
int indentLength = 2;
QByteArray 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>& models, QSet<QString>& remainingModels, QVector<QString>& modelIDs) {
if (remainingModels.contains(parentID)) {
modelIDs.append(parentID);
remainingModels.remove(parentID);
}
int parentIndex = modelIDs.size() - 1;
foreach (const QString& childID, connectionChildMap.values(parentID)) {
if (remainingModels.contains(childID)) {
FBXModel& model = models[childID];
if (model.parentIndex == -1) {
model.parentIndex = parentIndex;
appendModelIDs(childID, connectionChildMap, models, remainingModels, modelIDs);
}
}
}
}
FBXBlendshape extractBlendshape(const FBXNode& object) {
FBXBlendshape blendshape;
foreach (const FBXNode& data, object.children) {
if (data.name == "Indexes") {
blendshape.indices = FBXReader::getIntVector(data);
} else if (data.name == "Vertices") {
blendshape.vertices = FBXReader::createVec3Vector(FBXReader::getDoubleVector(data));
} else if (data.name == "Normals") {
blendshape.normals = FBXReader::createVec3Vector(FBXReader::getDoubleVector(data));
}
}
return blendshape;
}
void setTangents(FBXMesh& mesh, int firstIndex, int secondIndex) {
const glm::vec3& normal = mesh.normals.at(firstIndex);
glm::vec3 bitangent = glm::cross(normal, mesh.vertices.at(secondIndex) - mesh.vertices.at(firstIndex));
if (glm::length(bitangent) < EPSILON) {
return;
}
glm::vec2 texCoordDelta = mesh.texCoords.at(secondIndex) - mesh.texCoords.at(firstIndex);
glm::vec3 normalizedNormal = glm::normalize(normal);
mesh.tangents[firstIndex] += glm::cross(glm::angleAxis(-atan2f(-texCoordDelta.t, texCoordDelta.s), normalizedNormal) *
glm::normalize(bitangent), normalizedNormal);
}
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());
FBXBlendshape& 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>& models, 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 (models.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();
}
typedef std::vector<glm::vec3> ShapeVertices;
class AnimationCurve {
public:
QVector<float> values;
};
bool checkMaterialsHaveTextures(const QHash<QString, FBXMaterial>& materials,
const QHash<QString, QByteArray>& 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;
}
}
}
FBXLight extractLight(const FBXNode& object) {
FBXLight 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 = FBXReader::getVec3(property.properties, valIndex);
}
}
}
} else if ( subobject.name == "GeometryVersion"
|| subobject.name == "TypeFlags") {
}
}
#if defined(DEBUG_FBXREADER)
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;
}
QByteArray fileOnUrl(const QByteArray& 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);
}
FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) {
const FBXNode& node = _fbxNode;
QMap<QString, ExtractedMesh> meshes;
QHash<QString, QString> modelIDsToNames;
QHash<QString, int> meshIDsToMeshIndices;
QHash<QString, QString> ooChildToParent;
QVector<ExtractedBlendshape> blendshapes;
QHash<QString, FBXModel> models;
QHash<QString, Cluster> clusters;
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, FBXLight> lights;
QVariantHash joints = mapping.value("joint").toHash();
QString jointEyeLeftName = processID(getString(joints.value("jointEyeLeft", "jointEyeLeft")));
QString jointEyeRightName = processID(getString(joints.value("jointEyeRight", "jointEyeRight")));
QString jointNeckName = processID(getString(joints.value("jointNeck", "jointNeck")));
QString jointRootName = processID(getString(joints.value("jointRoot", "jointRoot")));
QString jointLeanName = processID(getString(joints.value("jointLean", "jointLean")));
QString jointHeadName = processID(getString(joints.value("jointHead", "jointHead")));
QString jointLeftHandName = processID(getString(joints.value("jointLeftHand", "jointLeftHand")));
QString jointRightHandName = processID(getString(joints.value("jointRightHand", "jointRightHand")));
QString jointEyeLeftID;
QString jointEyeRightID;
QString jointNeckID;
QString jointRootID;
QString jointLeanID;
QString jointHeadID;
QString jointLeftHandID;
QString jointRightHandID;
QString jointLeftToeID;
QString jointRightToeID;
QVector<QString> humanIKJointNames;
for (int i = 0;; i++) {
QByteArray jointName = HUMANIK_JOINTS[i];
if (jointName.isEmpty()) {
break;
}
humanIKJointNames.append(processID(getString(joints.value(jointName, jointName))));
}
QVector<QString> humanIKJointIDs(humanIKJointNames.size());
QVariantHash blendshapeMappings = mapping.value("bs").toHash();
QMultiHash<QByteArray, WeightedIndex> blendshapeIndices;
for (int i = 0;; i++) {
QByteArray 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_FBXREADER)
int unknown = 0;
#endif
FBXGeometry* geometryPtr = new FBXGeometry;
FBXGeometry& geometry = *geometryPtr;
geometry.originalURL = url;
float unitScaleFactor = 1.0f;
glm::vec3 ambientColor;
QString hifiGlobalNodeID;
unsigned int meshIndex = 0;
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") {
geometry.author = subsubobject.properties.at(0).toString();
}
}
} else if (subobject.name == "Properties70") {
foreach (const FBXNode& subsubobject, subobject.children) {
static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName"));
if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 &&
subsubobject.properties.at(0) == APPLICATION_NAME) {
geometry.applicationName = subsubobject.properties.at(4).toString();
}
}
}
}
}
}
} 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 = QByteArray("UnitScaleFactor");
static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor");
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 (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));
} else { // object.properties.at(2) == "Shape"
ExtractedBlendshape extracted = { getID(object.properties), extractBlendshape(object) };
blendshapes.append(extracted);
}
} else if (object.name == "Model") {
QString name = getName(object.properties);
QString id = getID(object.properties);
modelIDsToNames.insert(id, name);
QString modelname = name.toLower();
if (modelname.startsWith("hifi")) {
hifiGlobalNodeID = id;
}
if (name == jointEyeLeftName || name == "EyeL" || name == "joint_Leye") {
jointEyeLeftID = getID(object.properties);
} else if (name == jointEyeRightName || name == "EyeR" || name == "joint_Reye") {
jointEyeRightID = getID(object.properties);
} else if (name == jointNeckName || name == "NeckRot" || name == "joint_neck") {
jointNeckID = getID(object.properties);
} else if (name == jointRootName) {
jointRootID = getID(object.properties);
} else if (name == jointLeanName) {
jointLeanID = getID(object.properties);
} else if (name == jointHeadName) {
jointHeadID = getID(object.properties);
} else if (name == jointLeftHandName || name == "LeftHand" || name == "joint_L_hand") {
jointLeftHandID = getID(object.properties);
} else if (name == jointRightHandName || name == "RightHand" || name == "joint_R_hand") {
jointRightHandID = getID(object.properties);
} else if (name == "LeftToe" || name == "joint_L_toe" || name == "LeftToe_End") {
jointLeftToeID = getID(object.properties);
} else if (name == "RightToe" || name == "joint_R_toe" || name == "RightToe_End") {
jointRightToeID = getID(object.properties);
}
int humanIKJointIndex = humanIKJointNames.indexOf(name);
if (humanIKJointIndex != -1) {
humanIKJointIDs[humanIKJointIndex] = getID(object.properties);
}
glm::vec3 translation;
// NOTE: the euler angles as supplied by the FBX file are in degrees
glm::vec3 rotationOffset;
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;
FBXModel model = { 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) };
ExtractedMesh* mesh = NULL;
QVector<ExtractedBlendshape> blendshapes;
foreach (const FBXNode& subobject, object.children) {
bool properties = false;
QByteArray 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 GEOMETRIC_TRANSLATION = QByteArray("GeometricTranslation");
static const QVariant GEOMETRIC_ROTATION = QByteArray("GeometricRotation");
static const QVariant GEOMETRIC_SCALING = QByteArray("GeometricScaling");
static const QVariant LCL_TRANSLATION = QByteArray("Lcl Translation");
static const QVariant LCL_ROTATION = QByteArray("Lcl Rotation");
static const QVariant LCL_SCALING = QByteArray("Lcl Scaling");
static const QVariant ROTATION_MAX = QByteArray("RotationMax");
static const QVariant ROTATION_MAX_X = QByteArray("RotationMaxX");
static const QVariant ROTATION_MAX_Y = QByteArray("RotationMaxY");
static const QVariant ROTATION_MAX_Z = QByteArray("RotationMaxZ");
static const QVariant ROTATION_MIN = QByteArray("RotationMin");
static const QVariant ROTATION_MIN_X = QByteArray("RotationMinX");
static const QVariant ROTATION_MIN_Y = QByteArray("RotationMinY");
static const QVariant ROTATION_MIN_Z = QByteArray("RotationMinZ");
static const QVariant ROTATION_OFFSET = QByteArray("RotationOffset");
static const QVariant ROTATION_PIVOT = QByteArray("RotationPivot");
static const QVariant SCALING_OFFSET = QByteArray("ScalingOffset");
static const QVariant SCALING_PIVOT = QByteArray("ScalingPivot");
static const QVariant PRE_ROTATION = QByteArray("PreRotation");
static const QVariant POST_ROTATION = QByteArray("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_OFFSET) {
rotationOffset = getVec3(property.properties, index);
} else if (childProperty == ROTATION_PIVOT) {
rotationPivot = getVec3(property.properties, index);
} else if (childProperty == PRE_ROTATION) {
preRotation = getVec3(property.properties, index);
} else if (childProperty == LCL_ROTATION) {
rotation = getVec3(property.properties, index);
} else if (childProperty == POST_ROTATION) {
postRotation = 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") {
// it's a mesh as well as a model
mesh = &meshes[getID(object.properties)];
*mesh = extractMesh(object, meshIndex);
} else if (subobject.name == "Shape") {
ExtractedBlendshape blendshape = { subobject.properties.at(0).toString(),
extractBlendshape(subobject) };
blendshapes.append(blendshape);
}
#if defined(DEBUG_FBXREADER)
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();
}
FBXLight 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& extracted, blendshapes) {
addBlendshapes(extracted, blendshapeIndices.values(extracted.id.toLatin1()), *mesh);
}
}
// see FBX documentation, http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html
model.translation = translation;
model.preTransform = glm::translate(rotationOffset) * glm::translate(rotationPivot);
model.preRotation = glm::quat(glm::radians(preRotation));
model.rotation = glm::quat(glm::radians(rotation));
model.postRotation = glm::inverse(glm::quat(glm::radians(postRotation)));
model.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
model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f,
rotationMinY ? rotationMin.y : -180.0f, rotationMinZ ? rotationMin.z : -180.0f));
model.rotationMax = glm::radians(glm::vec3(rotationMaxX ? rotationMax.x : 180.0f,
rotationMaxY ? rotationMax.y : 180.0f, rotationMaxZ ? rotationMax.z : 180.0f));
model.hasGeometricOffset = hasGeometricOffset;
model.geometricTranslation = geometricTranslation;
model.geometricRotation = glm::quat(glm::radians(geometricRotation));
model.geometricScaling = geometricScaling;
models.insert(getID(object.properties), model);
} 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) {
QByteArray filename = subobject.properties.at(0).toByteArray();
QByteArray 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) {
tex.assign(tex.UVTranslation, glm::vec2(subobject.properties.at(0).value<double>(),
subobject.properties.at(1).value<double>()));
} else if (subobject.name == "ModelUVScaling" && subobject.properties.length() >= MODEL_UV_SCALING_MIN_SIZE) {
tex.assign(tex.UVScaling, glm::vec2(subobject.properties.at(0).value<double>(),
subobject.properties.at(1).value<double>()));
if (tex.UVScaling.x == 0.0f) {
tex.UVScaling.x = 1.0f;
}
if (tex.UVScaling.y == 0.0f) {
tex.UVScaling.y = 1.0f;
}
} 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") {
QByteArray propertyName;
int index;
propertyName = "P";
index = 4;
foreach (const FBXNode& property, subobject.children) {
static const QVariant UV_SET = QByteArray("UVSet");
static const QVariant CURRENT_TEXTURE_BLEND_MODE = QByteArray("CurrentTextureBlendMode");
static const QVariant USE_MATERIAL = QByteArray("UseMaterial");
static const QVariant TRANSLATION = QByteArray("Translation");
static const QVariant ROTATION = QByteArray("Rotation");
static const QVariant SCALING = QByteArray("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, 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) {
tex.assign(tex.scaling, getVec3(property.properties, index));
if (tex.scaling.x == 0.0f) {
tex.scaling.x = 1.0f;
}
if (tex.scaling.y == 0.0f) {
tex.scaling.y = 1.0f;
}
if (tex.scaling.z == 0.0f) {
tex.scaling.z = 1.0f;
}
}
#if defined(DEBUG_FBXREADER)
else {
QString propName = v;
unknown++;
}
#endif
}
}
}
#if defined(DEBUG_FBXREADER)
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") {
QByteArray filepath;
QByteArray 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") {
FBXMaterial material;
material.name = (object.properties.at(1).toString());
foreach (const FBXNode& subobject, object.children) {
bool properties = false;
QByteArray 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 = QByteArray("DiffuseColor");
static const QVariant DIFFUSE_FACTOR = QByteArray("DiffuseFactor");
static const QVariant DIFFUSE = QByteArray("Diffuse");
static const QVariant SPECULAR_COLOR = QByteArray("SpecularColor");
static const QVariant SPECULAR_FACTOR = QByteArray("SpecularFactor");
static const QVariant SPECULAR = QByteArray("Specular");
static const QVariant EMISSIVE_COLOR = QByteArray("EmissiveColor");
static const QVariant EMISSIVE_FACTOR = QByteArray("EmissiveFactor");
static const QVariant EMISSIVE = QByteArray("Emissive");
static const QVariant AMBIENT_FACTOR = QByteArray("AmbientFactor");
static const QVariant SHININESS = QByteArray("Shininess");
static const QVariant OPACITY = QByteArray("Opacity");
static const QVariant MAYA_USE_NORMAL_MAP = QByteArray("Maya|use_normal_map");
static const QVariant MAYA_BASE_COLOR = QByteArray("Maya|base_color");
static const QVariant MAYA_USE_COLOR_MAP = QByteArray("Maya|use_color_map");
static const QVariant MAYA_ROUGHNESS = QByteArray("Maya|roughness");
static const QVariant MAYA_USE_ROUGHNESS_MAP = QByteArray("Maya|use_roughness_map");
static const QVariant MAYA_METALLIC = QByteArray("Maya|metallic");
static const QVariant MAYA_USE_METALLIC_MAP = QByteArray("Maya|use_metallic_map");
static const QVariant MAYA_EMISSIVE = QByteArray("Maya|emissive");
static const QVariant MAYA_EMISSIVE_INTENSITY = QByteArray("Maya|emissive_intensity");
static const QVariant MAYA_USE_EMISSIVE_MAP = QByteArray("Maya|use_emissive_map");
static const QVariant MAYA_USE_AO_MAP = QByteArray("Maya|use_ao_map");
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 {
const QString propname = property.properties.at(0).toString();
unknowns.push_back(propname.toStdString());
}
}
}
}
#if defined(DEBUG_FBXREADER)
else {
QString propname = subobject.name.data();
int unknown = 0;
if ( (propname == "Version")
||(propname == "Multilayer")) {
} else {
unknown++;
}
}
#endif
}
material.materialID = getID(object.properties);
_fbxMaterials.insert(material.materialID, material);
} else if (object.name == "NodeAttribute") {
#if defined(DEBUG_FBXREADER)
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") {
FBXLight 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);
}
}
clusters.insert(getID(object.properties), cluster);
} else if (object.properties.last() == "BlendShapeChannel") {
QByteArray 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);
geometry.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_FBXREADER)
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 = QByteArray("OO");
static const QVariant OP = QByteArray("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, FBXLight >::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;
QByteArray 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++;
}
}
_connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2));
_connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1));
}
}
}
#if defined(DEBUG_FBXREADER)
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& extracted, blendshapes) {
QString blendshapeChannelID = _connectionParentMap.value(extracted.id);
QString blendshapeID = _connectionParentMap.value(blendshapeChannelID);
QString meshID = _connectionParentMap.value(blendshapeID);
addBlendshapes(extracted, 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())));
geometry.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> remainingModels;
for (QHash<QString, FBXModel>::const_iterator model = models.constBegin(); model != models.constEnd(); model++) {
// models with clusters must be parented to the cluster top
foreach (const QString& deformerID, _connectionChildMap.values(model.key())) {
foreach (const QString& clusterID, _connectionChildMap.values(deformerID)) {
if (!clusters.contains(clusterID)) {
continue;
}
QString topID = getTopModelID(_connectionParentMap, models, _connectionChildMap.value(clusterID), url);
_connectionChildMap.remove(_connectionParentMap.take(model.key()), model.key());
_connectionParentMap.insert(model.key(), topID);
goto outerBreak;
}
}
outerBreak:
// make sure the parent is in the child map
QString parent = _connectionParentMap.value(model.key());
if (!_connectionChildMap.contains(parent, model.key())) {
_connectionChildMap.insert(parent, model.key());
}
remainingModels.insert(model.key());
}
while (!remainingModels.isEmpty()) {
QString first = *remainingModels.constBegin();
foreach (const QString& id, remainingModels) {
if (id < first) {
first = id;
}
}
QString topID = getTopModelID(_connectionParentMap, models, first, url);
appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, models, remainingModels, modelIDs);
}
// 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++) {
FBXAnimationFrame frame;
frame.rotations.resize(modelIDs.size());
frame.translations.resize(modelIDs.size());
geometry.animationFrames.append(frame);
}
// convert the models to joints
QVariantList freeJoints = mapping.values("freeJoint");
geometry.hasSkeletonJoints = false;
foreach (const QString& modelID, modelIDs) {
const FBXModel& model = models[modelID];
FBXJoint joint;
joint.isFree = freeJoints.contains(model.name);
joint.parentIndex = model.parentIndex;
// get the indices of all ancestors starting with the first free one (if any)
int jointIndex = geometry.joints.size();
joint.freeLineage.append(jointIndex);
int lastFreeIndex = joint.isFree ? 0 : -1;
for (int index = joint.parentIndex; index != -1; index = geometry.joints.at(index).parentIndex) {
if (geometry.joints.at(index).isFree) {
lastFreeIndex = joint.freeLineage.size();
}
joint.freeLineage.append(index);
}
joint.freeLineage.remove(lastFreeIndex + 1, joint.freeLineage.size() - lastFreeIndex - 1);
joint.translation = model.translation; // these are usually in centimeters
joint.preTransform = model.preTransform;
joint.preRotation = model.preRotation;
joint.rotation = model.rotation;
joint.postRotation = model.postRotation;
joint.postTransform = model.postTransform;
joint.rotationMin = model.rotationMin;
joint.rotationMax = model.rotationMax;
joint.hasGeometricOffset = model.hasGeometricOffset;
joint.geometricTranslation = model.geometricTranslation;
joint.geometricRotation = model.geometricRotation;
joint.geometricScaling = model.geometricScaling;
glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation;
if (joint.parentIndex == -1) {
joint.transform = geometry.offset * glm::translate(joint.translation) * joint.preTransform *
glm::mat4_cast(combinedRotation) * joint.postTransform;
joint.inverseDefaultRotation = glm::inverse(combinedRotation);
joint.distanceToParent = 0.0f;
} else {
const FBXJoint& parentJoint = geometry.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 = model.name;
foreach (const QString& childID, _connectionChildMap.values(modelID)) {
QString type = typeFlags.value(childID);
if (!type.isEmpty()) {
geometry.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton"));
break;
}
}
joint.bindTransformFoundInCluster = false;
geometry.joints.append(joint);
geometry.jointIndices.insert(model.name, geometry.joints.size());
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++) {
geometry.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()))));
geometry.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()));
}
}
// NOTE: shapeVertices are in joint-frame
std::vector<ShapeVertices> shapeVertices;
shapeVertices.resize(geometry.joints.size());
// find our special joints
geometry.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID);
geometry.rightEyeJointIndex = modelIDs.indexOf(jointEyeRightID);
geometry.neckJointIndex = modelIDs.indexOf(jointNeckID);
geometry.rootJointIndex = modelIDs.indexOf(jointRootID);
geometry.leanJointIndex = modelIDs.indexOf(jointLeanID);
geometry.headJointIndex = modelIDs.indexOf(jointHeadID);
geometry.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID);
geometry.rightHandJointIndex = modelIDs.indexOf(jointRightHandID);
geometry.leftToeJointIndex = modelIDs.indexOf(jointLeftToeID);
geometry.rightToeJointIndex = modelIDs.indexOf(jointRightToeID);
foreach (const QString& id, humanIKJointIDs) {
geometry.humanIKJointIndices.append(modelIDs.indexOf(id));
}
// extract the translation component of the neck transform
if (geometry.neckJointIndex != -1) {
const glm::mat4& transform = geometry.joints.at(geometry.neckJointIndex).transform;
geometry.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]);
}
geometry.bindExtents.reset();
geometry.meshExtents.reset();
// Create the Material Library
consolidateFBXMaterials(mapping);
// 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 : _fbxMaterials) {
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 : _fbxMaterials) {
material.setMaxNumPixelsPerTexture(maxWidth * maxWidth);
}
}
#endif
geometry.materials = _fbxMaterials;
// see if any materials have texture children
bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, _textureFilenames, _connectionChildMap);
for (QMap<QString, ExtractedMesh>::iterator it = meshes.begin(); it != meshes.end(); it++) {
ExtractedMesh& extracted = it.value();
extracted.mesh.meshExtents.reset();
// accumulate local transforms
QString modelID = models.contains(it.key()) ? it.key() : _connectionParentMap.value(it.key());
glm::mat4 modelTransform = getGlobalTransform(_connectionParentMap, models, modelID, geometry.applicationName == "mixamo.com", url);
// compute the mesh extents from the transformed vertices
foreach (const glm::vec3& vertex, extracted.mesh.vertices) {
glm::vec3 transformedVertex = glm::vec3(modelTransform * glm::vec4(vertex, 1.0f));
geometry.meshExtents.minimum = glm::min(geometry.meshExtents.minimum, transformedVertex);
geometry.meshExtents.maximum = glm::max(geometry.meshExtents.maximum, transformedVertex);
extracted.mesh.meshExtents.minimum = glm::min(extracted.mesh.meshExtents.minimum, transformedVertex);
extracted.mesh.meshExtents.maximum = glm::max(extracted.mesh.meshExtents.maximum, transformedVertex);
extracted.mesh.modelTransform = modelTransform;
}
// look for textures, material properties
// allocate the Part material library
int materialIndex = 0;
int textureIndex = 0;
bool generateTangents = false;
QList<QString> children = _connectionChildMap.values(modelID);
for (int i = children.size() - 1; i >= 0; i--) {
const QString& childID = children.at(i);
if (_fbxMaterials.contains(childID)) {
// the pure material associated with this part
FBXMaterial material = _fbxMaterials.value(childID);
for (int j = 0; j < extracted.partMaterialTextures.size(); j++) {
if (extracted.partMaterialTextures.at(j).first == materialIndex) {
FBXMeshPart& part = extracted.mesh.parts[j];
part.materialID = material.materialID;
generateTangents |= material.needTangentSpace();
}
}
materialIndex++;
} else if (_textureFilenames.contains(childID)) {
FBXTexture texture = getTexture(childID);
for (int j = 0; j < extracted.partMaterialTextures.size(); j++) {
int partTexture = extracted.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?
// extracted.mesh.parts[j].diffuseTexture = texture;
}
}
textureIndex++;
}
}
// if we have a normal map (and texture coordinates), we must compute tangents
if (generateTangents && !extracted.mesh.texCoords.isEmpty()) {
extracted.mesh.tangents.resize(extracted.mesh.vertices.size());
foreach (const FBXMeshPart& part, extracted.mesh.parts) {
for (int i = 0; i < part.quadIndices.size(); i += 4) {
setTangents(extracted.mesh, part.quadIndices.at(i), part.quadIndices.at(i + 1));
setTangents(extracted.mesh, part.quadIndices.at(i + 1), part.quadIndices.at(i + 2));
setTangents(extracted.mesh, part.quadIndices.at(i + 2), part.quadIndices.at(i + 3));
setTangents(extracted.mesh, part.quadIndices.at(i + 3), part.quadIndices.at(i));
}
// <= size - 3 in order to prevent overflowing triangleIndices when (i % 3) != 0
// This is most likely evidence of a further problem in extractMesh()
for (int i = 0; i <= part.triangleIndices.size() - 3; i += 3) {
setTangents(extracted.mesh, part.triangleIndices.at(i), part.triangleIndices.at(i + 1));
setTangents(extracted.mesh, part.triangleIndices.at(i + 1), part.triangleIndices.at(i + 2));
setTangents(extracted.mesh, part.triangleIndices.at(i + 2), part.triangleIndices.at(i));
}
if ((part.triangleIndices.size() % 3) != 0){
qCDebug(modelformat) << "Error in extractFBXGeometry part.triangleIndices.size() is not divisible by three ";
}
}
}
// find the clusters with which the mesh is associated
QVector<QString> clusterIDs;
foreach (const QString& childID, _connectionChildMap.values(it.key())) {
foreach (const QString& clusterID, _connectionChildMap.values(childID)) {
if (!clusters.contains(clusterID)) {
continue;
}
FBXCluster fbxCluster;
const Cluster& cluster = clusters[clusterID];
clusterIDs.append(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);
fbxCluster.jointIndex = modelIDs.indexOf(jointID);
if (fbxCluster.jointIndex == -1) {
qCDebug(modelformat) << "Joint not in model list: " << jointID;
fbxCluster.jointIndex = 0;
}
fbxCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform;
extracted.mesh.clusters.append(fbxCluster);
// override the bind rotation with the transform link
FBXJoint& joint = geometry.joints[fbxCluster.jointIndex];
joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink));
joint.bindTransform = cluster.transformLink;
joint.bindTransformFoundInCluster = true;
// update the bind pose extents
glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform);
geometry.bindExtents.addPoint(bindTranslation);
}
}
// if we don't have a skinned joint, parent to the model itself
if (extracted.mesh.clusters.isEmpty()) {
FBXCluster cluster;
cluster.jointIndex = modelIDs.indexOf(modelID);
if (cluster.jointIndex == -1) {
qCDebug(modelformat) << "Model not in model list: " << modelID;
cluster.jointIndex = 0;
}
extracted.mesh.clusters.append(cluster);
}
// whether we're skinned depends on how many clusters are attached
const FBXCluster& firstFBXCluster = extracted.mesh.clusters.at(0);
glm::mat4 inverseModelTransform = glm::inverse(modelTransform);
if (clusterIDs.size() > 1) {
// this is a multi-mesh joint
const int WEIGHTS_PER_VERTEX = 4;
int numClusterIndices = extracted.mesh.vertices.size() * WEIGHTS_PER_VERTEX;
extracted.mesh.clusterIndices.fill(0, numClusterIndices);
QVector<float> weightAccumulators;
weightAccumulators.fill(0.0f, numClusterIndices);
for (int i = 0; i < clusterIDs.size(); i++) {
QString clusterID = clusterIDs.at(i);
const Cluster& cluster = clusters[clusterID];
const FBXCluster& fbxCluster = extracted.mesh.clusters.at(i);
int jointIndex = fbxCluster.jointIndex;
FBXJoint& joint = geometry.joints[jointIndex];
glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform;
glm::vec3 boneEnd = extractTranslation(transformJointToMesh);
glm::vec3 boneBegin = boneEnd;
glm::vec3 boneDirection;
float boneLength = 0.0f;
if (joint.parentIndex != -1) {
boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform);
boneDirection = boneEnd - boneBegin;
boneLength = glm::length(boneDirection);
if (boneLength > EPSILON) {
boneDirection /= boneLength;
}
}
float clusterScale = extractUniformScale(fbxCluster.inverseBindMatrix);
glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform;
ShapeVertices& points = shapeVertices.at(jointIndex);
for (int j = 0; j < cluster.indices.size(); j++) {
int oldIndex = cluster.indices.at(j);
float weight = cluster.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();
// remember vertices with at least 1/4 weight
const float EXPANSION_WEIGHT_THRESHOLD = 0.99f;
if (weight > EXPANSION_WEIGHT_THRESHOLD) {
// transform to joint-frame and save for later
const glm::mat4 vertexTransform = meshToJoint * glm::translate(extracted.mesh.vertices.at(newIndex));
points.push_back(extractTranslation(vertexTransform) * clusterScale);
}
// look for an unused slot in the weights vector
int weightIndex = newIndex * WEIGHTS_PER_VERTEX;
int lowestIndex = -1;
float lowestWeight = FLT_MAX;
int k = 0;
for (; k < WEIGHTS_PER_VERTEX; k++) {
if (weightAccumulators[weightIndex + k] == 0.0f) {
extracted.mesh.clusterIndices[weightIndex + k] = i;
weightAccumulators[weightIndex + k] = weight;
break;
}
if (weightAccumulators[weightIndex + k] < lowestWeight) {
lowestIndex = k;
lowestWeight = weightAccumulators[weightIndex + k];
}
}
if (k == WEIGHTS_PER_VERTEX && weight > lowestWeight) {
// no space for an additional weight; we must replace the lowest
weightAccumulators[weightIndex + lowestIndex] = weight;
extracted.mesh.clusterIndices[weightIndex + lowestIndex] = i;
}
}
}
}
// now that we've accumulated the most relevant weights for each vertex
// normalize and compress to 8-bits
extracted.mesh.clusterWeights.fill(0, numClusterIndices);
int numVertices = extracted.mesh.vertices.size();
for (int i = 0; i < numVertices; ++i) {
int j = i * WEIGHTS_PER_VERTEX;
// normalize weights into uint8_t
float totalWeight = weightAccumulators[j];
for (int k = j + 1; k < j + WEIGHTS_PER_VERTEX; ++k) {
totalWeight += weightAccumulators[k];
}
if (totalWeight > 0.0f) {
const float ALMOST_HALF = 0.499f;
float weightScalingFactor = (float)(UINT8_MAX) / totalWeight;
for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) {
extracted.mesh.clusterWeights[k] = (uint8_t)(weightScalingFactor * weightAccumulators[k] + ALMOST_HALF);
}
}
}
} else {
// this is a single-mesh joint
int jointIndex = firstFBXCluster.jointIndex;
FBXJoint& joint = geometry.joints[jointIndex];
// transform cluster vertices to joint-frame and save for later
float clusterScale = extractUniformScale(firstFBXCluster.inverseBindMatrix);
glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform;
ShapeVertices& points = shapeVertices.at(jointIndex);
foreach (const glm::vec3& vertex, extracted.mesh.vertices) {
const glm::mat4 vertexTransform = meshToJoint * glm::translate(vertex);
points.push_back(extractTranslation(vertexTransform) * clusterScale);
}
// 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 < extracted.mesh.vertices.size(); i++) {
extracted.mesh.vertices[i] = transformPoint(geometricOffset, extracted.mesh.vertices[i]);
}
}
}
buildModelMesh(extracted.mesh, url);
geometry.meshes.append(extracted.mesh);
int meshIndex = geometry.meshes.size() - 1;
meshIDsToMeshIndices.insert(it.key(), meshIndex);
}
const float INV_SQRT_3 = 0.57735026918f;
ShapeVertices cardinalDirections = {
Vectors::UNIT_X,
Vectors::UNIT_Y,
Vectors::UNIT_Z,
glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3),
glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3),
glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3)
};
// now that all joints have been scanned compute a k-Dop bounding volume of mesh
glm::vec3 defaultCapsuleAxis(0.0f, 1.0f, 0.0f);
for (int i = 0; i < geometry.joints.size(); ++i) {
FBXJoint& joint = geometry.joints[i];
// NOTE: points are in joint-frame
ShapeVertices& points = shapeVertices.at(i);
if (points.size() > 0) {
// compute average point
glm::vec3 avgPoint = glm::vec3(0.0f);
for (uint32_t j = 0; j < points.size(); ++j) {
avgPoint += points[j];
}
avgPoint /= (float)points.size();
// compute a k-Dop bounding volume
for (uint32_t j = 0; j < cardinalDirections.size(); ++j) {
float maxDot = -FLT_MAX;
float minDot = FLT_MIN;
for (uint32_t k = 0; k < points.size(); ++k) {
float kDot = glm::dot(cardinalDirections[j], points[k] - avgPoint);
if (kDot > maxDot) {
maxDot = kDot;
}
if (kDot < minDot) {
minDot = kDot;
}
}
joint.shapeInfo.points.push_back(avgPoint + maxDot * cardinalDirections[j]);
joint.shapeInfo.points.push_back(avgPoint + minDot * cardinalDirections[j]);
}
}
}
geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString());
// 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);
geometry.meshIndicesToModelNames.insert(meshIndex, modelName);
}
}
}
return geometryPtr;
}
FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) {
QBuffer buffer(const_cast<QByteArray*>(&model));
buffer.open(QIODevice::ReadOnly);
return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel);
}
FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) {
FBXReader reader;
reader._fbxNode = FBXReader::parseFBX(device);
reader._loadLightmaps = loadLightmaps;
reader._lightmapLevel = lightmapLevel;
return reader.extractFBXGeometry(mapping, url);
}