overte-HifiExperiments/libraries/fbx/src/OBJReader.cpp
2015-12-15 19:30:15 -08:00

648 lines
28 KiB
C++

//
// OBJReader.cpp
// libraries/fbx/src/
//
// Created by Seth Alves on 3/7/15.
// 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
//
// http://en.wikipedia.org/wiki/Wavefront_.obj_file
// http://www.scratchapixel.com/old/lessons/3d-advanced-lessons/obj-file-format/obj-file-format/
// http://paulbourke.net/dataformats/obj/
#include <QBuffer>
#include <QIODevice>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QEventLoop>
#include <ctype.h> // .obj files are not locale-specific. The C/ASCII charset applies.
#include <NetworkAccessManager.h>
#include "FBXReader.h"
#include "OBJReader.h"
#include "ModelFormatLogging.h"
QHash<QString, float> COMMENT_SCALE_HINTS = {{"This file uses centimeters as units", 1.0f / 100.0f},
{"This file uses millimeters as units", 1.0f / 1000.0f}};
const QString SMART_DEFAULT_MATERIAL_NAME = "High Fidelity smart default material name";
OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) {
}
const QByteArray OBJTokenizer::getLineAsDatum() {
return _device->readLine().trimmed();
}
int OBJTokenizer::nextToken() {
if (_pushedBackToken != NO_PUSHBACKED_TOKEN) {
int token = _pushedBackToken;
_pushedBackToken = NO_PUSHBACKED_TOKEN;
return token;
}
char ch;
while (_device->getChar(&ch)) {
if (QChar(ch).isSpace()) {
continue; // skip whitespace
}
switch (ch) {
case '#': {
_comment = _device->readLine(); // stash comment for a future call to getComment
return COMMENT_TOKEN;
}
case '\"':
_datum = "";
while (_device->getChar(&ch)) {
if (ch == '\"') { // end on closing quote
break;
}
if (ch == '\\') { // handle escaped quotes
if (_device->getChar(&ch) && ch != '\"') {
_datum.append('\\');
}
}
_datum.append(ch);
}
return DATUM_TOKEN;
default:
_datum = "";
_datum.append(ch);
while (_device->getChar(&ch)) {
if (QChar(ch).isSpace() || ch == '\"') {
ungetChar(ch); // read until we encounter a special character, then replace it
break;
}
_datum.append(ch);
}
return DATUM_TOKEN;
}
}
return NO_TOKEN;
}
bool OBJTokenizer::isNextTokenFloat() {
if (nextToken() != OBJTokenizer::DATUM_TOKEN) {
return false;
}
QByteArray token = getDatum();
pushBackToken(OBJTokenizer::DATUM_TOKEN);
bool ok;
token.toFloat(&ok);
return ok;
}
glm::vec3 OBJTokenizer::getVec3() {
auto x = getFloat(); // N.B.: getFloat() has side-effect
auto y = getFloat(); // And order of arguments is different on Windows/Linux.
auto z = getFloat();
auto v = glm::vec3(x, y, z);
while (isNextTokenFloat()) {
// the spec(s) get(s) vague here. might be w, might be a color... chop it off.
nextToken();
}
return v;
}
glm::vec2 OBJTokenizer::getVec2() {
auto v = glm::vec2(getFloat(), 1.0f - getFloat()); // OBJ has an odd sense of u, v. Also N.B.: getFloat() has side-effect
while (isNextTokenFloat()) {
// there can be a w, but we don't handle that
nextToken();
}
return v;
}
void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) {
meshPart.materialID = materialID;
}
// OBJFace
bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector<glm::vec3>& vertices) {
bool ok;
int index = vertexIndex.toInt(&ok);
if (!ok) {
return false;
}
vertexIndices.append(index - 1);
if (!textureIndex.isEmpty()) {
index = textureIndex.toInt(&ok);
if (!ok) {
return false;
}
if (index < 0) { // Count backwards from the last one added.
index = vertices.count() + 1 + index;
}
textureUVIndices.append(index - 1);
}
if (!normalIndex.isEmpty()) {
index = normalIndex.toInt(&ok);
if (!ok) {
return false;
}
normalIndices.append(index - 1);
}
return true;
}
QVector<OBJFace> OBJFace::triangulate() {
QVector<OBJFace> newFaces;
const int nVerticesInATriangle = 3;
if (vertexIndices.count() == nVerticesInATriangle) {
newFaces.append(*this);
} else {
for (int i = 1; i < vertexIndices.count() - 1; i++) {
OBJFace newFace;
newFace.addFrom(this, 0);
newFace.addFrom(this, i);
newFace.addFrom(this, i + 1);
newFace.groupName = groupName;
newFace.materialName = materialName;
newFaces.append(newFace);
}
}
return newFaces;
}
void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f at index i
vertexIndices.append(face->vertexIndices[index]);
if (face->textureUVIndices.count() > 0) { // Any at all. Runtime error if not consistent.
textureUVIndices.append(face->textureUVIndices[index]);
}
if (face->normalIndices.count() > 0) {
normalIndices.append(face->normalIndices[index]);
}
}
bool OBJReader::isValidTexture(const QByteArray &filename) {
if (_url.isEmpty()) {
return false;
}
QUrl candidateUrl = _url.resolved(QUrl(filename));
QNetworkReply *netReply = request(candidateUrl, true);
bool isValid = netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200);
netReply->deleteLater();
return isValid;
}
void OBJReader::parseMaterialLibrary(QIODevice* device) {
OBJTokenizer tokenizer(device);
QString matName = SMART_DEFAULT_MATERIAL_NAME;
OBJMaterial& currentMaterial = materials[matName];
while (true) {
switch (tokenizer.nextToken()) {
case OBJTokenizer::COMMENT_TOKEN:
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader MTLLIB comment:" << tokenizer.getComment();
#endif
break;
case OBJTokenizer::DATUM_TOKEN:
break;
default:
materials[matName] = currentMaterial;
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader Last material shininess:" << currentMaterial.shininess << " opacity:" << currentMaterial.opacity << " diffuse color:" << currentMaterial.diffuseColor << " specular color:" << currentMaterial.specularColor << " diffuse texture:" << currentMaterial.diffuseTextureFilename << " specular texture:" << currentMaterial.specularTextureFilename;
#endif
return;
}
QByteArray token = tokenizer.getDatum();
if (token == "newmtl") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
return;
}
materials[matName] = currentMaterial;
matName = tokenizer.getDatum();
currentMaterial = materials[matName];
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader Starting new material definition " << matName;
#endif
currentMaterial.diffuseTextureFilename = "";
} else if (token == "Ns") {
currentMaterial.shininess = tokenizer.getFloat();
} else if ((token == "d") || (token == "Tr")) {
currentMaterial.opacity = tokenizer.getFloat();
} else if (token == "Ka") {
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader Ignoring material Ka " << tokenizer.getVec3();
#endif
} else if (token == "Kd") {
currentMaterial.diffuseColor = tokenizer.getVec3();
} else if (token == "Ks") {
currentMaterial.specularColor = tokenizer.getVec3();
} else if ((token == "map_Kd") || (token == "map_Ks")) {
QByteArray filename = QUrl(tokenizer.getLineAsDatum()).fileName().toUtf8();
if (filename.endsWith(".tga")) {
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader WARNING: currently ignoring tga texture " << filename << " in " << _url;
#endif
break;
}
if (isValidTexture(filename)) {
if (token == "map_Kd") {
currentMaterial.diffuseTextureFilename = filename;
} else {
currentMaterial.specularTextureFilename = filename;
}
} else {
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader WARNING: " << _url << " ignoring missing texture " << filename;
#endif
}
}
}
}
QNetworkReply* OBJReader::request(QUrl& url, bool isTest) {
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest netRequest(url);
QNetworkReply* netReply = isTest ? networkAccessManager.head(netRequest) : networkAccessManager.get(netRequest);
QEventLoop loop; // Create an event loop that will quit when we get the finished signal
QObject::connect(netReply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec(); // Nothing is going to happen on this whole run thread until we get this
netReply->waitForReadyRead(-1); // so we might as well block this thread waiting for the response, rather than
return netReply; // trying to sync later on.
}
bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess) {
FaceGroup faces;
FBXMesh& mesh = geometry.meshes[0];
mesh.parts.append(FBXMeshPart());
FBXMeshPart& meshPart = mesh.parts.last();
bool sawG = false;
bool result = true;
int originalFaceCountForDebugging = 0;
QString currentGroup;
setMeshPartDefaults(meshPart, QString("dontknow") + QString::number(mesh.parts.count()));
while (true) {
int tokenType = tokenizer.nextToken();
if (tokenType == OBJTokenizer::COMMENT_TOKEN) {
// loop through the list of known comments which suggest a scaling factor.
// if we find one, save the scaling hint into scaleGuess
QString comment = tokenizer.getComment();
QHashIterator<QString, float> i(COMMENT_SCALE_HINTS);
while (i.hasNext()) {
i.next();
if (comment.contains(i.key())) {
scaleGuess = i.value();
}
}
continue;
}
if (tokenType != OBJTokenizer::DATUM_TOKEN) {
result = false;
break;
}
QByteArray token = tokenizer.getDatum();
//qCDebug(modelformat) << token;
// we don't support separate objects in the same file, so treat "o" the same as "g".
if (token == "g" || token == "o") {
if (sawG) {
// we've encountered the beginning of the next group.
tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN);
break;
}
sawG = true;
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
QByteArray groupName = tokenizer.getDatum();
currentGroup = groupName;
} else if (token == "mtllib" && !_url.isEmpty()) {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
QByteArray libraryName = tokenizer.getDatum();
if (librariesSeen.contains(libraryName)) {
break; // Some files use mtllib over and over again for the same libraryName
}
librariesSeen[libraryName] = true;
// Throw away any path part of libraryName, and merge against original url.
QUrl libraryUrl = _url.resolved(QUrl(libraryName).fileName());
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader new library:" << libraryName << " at:" << libraryUrl;
#endif
QNetworkReply* netReply = request(libraryUrl, false);
if (netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200)) {
parseMaterialLibrary(netReply);
} else {
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader " << libraryName << " did not answer. Got "
<< netReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
#endif
}
netReply->deleteLater();
} else if (token == "usemtl") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
currentMaterialName = tokenizer.getDatum();
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader new current material:" << currentMaterialName;
#endif
} else if (token == "v") {
vertices.append(tokenizer.getVec3());
} else if (token == "vn") {
normals.append(tokenizer.getVec3());
} else if (token == "vt") {
textureUVs.append(tokenizer.getVec2());
} else if (token == "f") {
OBJFace face;
while (true) {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
if (face.vertexIndices.count() == 0) {
// nonsense, bail out.
goto done;
}
break;
}
// faces can be:
// vertex-index
// vertex-index/texture-index
// vertex-index/texture-index/surface-normal-index
QByteArray token = tokenizer.getDatum();
if (!isdigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done;
tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN);
break;
}
QList<QByteArray> parts = token.split('/');
assert(parts.count() >= 1);
assert(parts.count() <= 3);
const QByteArray noData {};
face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices);
face.groupName = currentGroup;
face.materialName = currentMaterialName;
}
originalFaceCountForDebugging++;
foreach(OBJFace face, face.triangulate()) {
faces.append(face);
}
} else {
// something we don't (yet) care about
// qCDebug(modelformat) << "OBJ parser is skipping a line with" << token;
tokenizer.skipLine();
}
}
done:
if (faces.count() == 0) { // empty mesh
mesh.parts.pop_back();
} else {
faceGroups.append(faces); // We're done with this group. Add the faces.
}
return result;
}
FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, const QUrl& url) {
QBuffer buffer { &model };
buffer.open(QIODevice::ReadOnly);
FBXGeometry* geometryPtr = new FBXGeometry();
FBXGeometry& geometry = *geometryPtr;
OBJTokenizer tokenizer { &buffer };
float scaleGuess = 1.0f;
_url = url;
geometry.meshExtents.reset();
geometry.meshes.append(FBXMesh());
try {
// call parseOBJGroup as long as it's returning true. Each successful call will
// add a new meshPart to the geometry's single mesh.
while (parseOBJGroup(tokenizer, mapping, geometry, scaleGuess)) {}
FBXMesh& mesh = geometry.meshes[0];
mesh.meshIndex = 0;
geometry.joints.resize(1);
geometry.joints[0].isFree = false;
geometry.joints[0].parentIndex = -1;
geometry.joints[0].distanceToParent = 0;
geometry.joints[0].translation = glm::vec3(0, 0, 0);
geometry.joints[0].rotationMin = glm::vec3(0, 0, 0);
geometry.joints[0].rotationMax = glm::vec3(0, 0, 0);
geometry.joints[0].name = "OBJ";
geometry.joints[0].isSkeletonJoint = true;
geometry.jointIndices["x"] = 1;
FBXCluster cluster;
cluster.jointIndex = 0;
cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
mesh.clusters.append(cluster);
// Some .obj files use the convention that a group with uv coordinates that doesn't define a material, should use
// a texture with the same basename as the .obj file.
if (!url.isEmpty()) {
QString filename = url.fileName();
int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail
QString basename = filename.remove(extIndex + 1, sizeof("obj"));
OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME];
preDefinedMaterial.diffuseColor = glm::vec3(1.0f);
QVector<QByteArray> extensions = {"jpg", "jpeg", "png", "tga"};
QByteArray base = basename.toUtf8(), textName = "";
for (int i = 0; i < extensions.count(); i++) {
QByteArray candidateString = base + extensions[i];
if (isValidTexture(candidateString)) {
textName = candidateString;
break;
}
}
if (!textName.isEmpty()) {
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader found a default texture: " << textName;
#endif
preDefinedMaterial.diffuseTextureFilename = textName;
}
materials[SMART_DEFAULT_MATERIAL_NAME] = preDefinedMaterial;
}
for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) {
FBXMeshPart& meshPart = mesh.parts[i];
FaceGroup faceGroup = faceGroups[meshPartCount];
OBJFace leadFace = faceGroup[0]; // All the faces in the same group will have the same name and material.
QString groupMaterialName = leadFace.materialName;
if (groupMaterialName.isEmpty() && (leadFace.textureUVIndices.count() > 0)) {
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader WARNING: " << url
<< " needs a texture that isn't specified. Using default mechanism.";
#endif
groupMaterialName = SMART_DEFAULT_MATERIAL_NAME;
} else if (!groupMaterialName.isEmpty() && !materials.contains(groupMaterialName)) {
#ifdef WANT_DEBUG
qCDebug(modelformat) << "OBJ Reader WARNING: " << url
<< " specifies a material " << groupMaterialName
<< " that is not defined. Using default mechanism.";
#endif
groupMaterialName = SMART_DEFAULT_MATERIAL_NAME;
}
if (!groupMaterialName.isEmpty()) {
meshPart.materialID = groupMaterialName;
}
foreach(OBJFace face, faceGroup) {
glm::vec3 v0 = vertices[face.vertexIndices[0]];
glm::vec3 v1 = vertices[face.vertexIndices[1]];
glm::vec3 v2 = vertices[face.vertexIndices[2]];
meshPart.triangleIndices.append(mesh.vertices.count()); // not face.vertexIndices into vertices
mesh.vertices << v0;
meshPart.triangleIndices.append(mesh.vertices.count());
mesh.vertices << v1;
meshPart.triangleIndices.append(mesh.vertices.count());
mesh.vertices << v2;
glm::vec3 n0, n1, n2;
if (face.normalIndices.count()) {
n0 = normals[face.normalIndices[0]];
n1 = normals[face.normalIndices[1]];
n2 = normals[face.normalIndices[2]];
} else { // generate normals from triangle plane if not provided
n0 = n1 = n2 = glm::cross(v1 - v0, v2 - v0);
}
mesh.normals << n0 << n1 << n2;
if (face.textureUVIndices.count()) {
mesh.texCoords
<< textureUVs[face.textureUVIndices[0]]
<< textureUVs[face.textureUVIndices[1]]
<< textureUVs[face.textureUVIndices[2]];
} else {
glm::vec2 corner(0.0f, 1.0f);
mesh.texCoords << corner << corner << corner;
}
}
}
// if we got a hint about units, scale all the points
if (scaleGuess != 1.0f) {
for (int i = 0; i < mesh.vertices.size(); i++) {
mesh.vertices[i] *= scaleGuess;
}
}
mesh.meshExtents.reset();
foreach (const glm::vec3& vertex, mesh.vertices) {
mesh.meshExtents.addPoint(vertex);
geometry.meshExtents.addPoint(vertex);
}
FBXReader::buildModelMesh(mesh, url.toString());
// fbxDebugDump(geometry);
} catch(const std::exception& e) {
qCDebug(modelformat) << "OBJ reader fail: " << e.what();
}
foreach (QString materialID, materials.keys()) {
OBJMaterial& objMaterial = materials[materialID];
geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor,
objMaterial.specularColor,
glm::vec3(0.0f),
glm::vec2(0.0f, 1.0f),
objMaterial.shininess,
objMaterial.opacity);
FBXMaterial& fbxMaterial = geometry.materials[materialID];
fbxMaterial.materialID = materialID;
fbxMaterial._material = std::make_shared<model::Material>();
model::MaterialPointer modelMaterial = fbxMaterial._material;
if (!objMaterial.diffuseTextureFilename.isEmpty()) {
fbxMaterial.diffuseTexture.filename = objMaterial.diffuseTextureFilename;
}
modelMaterial->setEmissive(fbxMaterial.emissiveColor);
modelMaterial->setDiffuse(fbxMaterial.diffuseColor);
modelMaterial->setMetallic(glm::length(fbxMaterial.specularColor));
modelMaterial->setGloss(fbxMaterial.shininess);
if (fbxMaterial.opacity <= 0.0f) {
modelMaterial->setOpacity(1.0f);
} else {
modelMaterial->setOpacity(fbxMaterial.opacity);
}
}
return geometryPtr;
}
void fbxDebugDump(const FBXGeometry& fbxgeo) {
qCDebug(modelformat) << "---------------- fbxGeometry ----------------";
qCDebug(modelformat) << " hasSkeletonJoints =" << fbxgeo.hasSkeletonJoints;
qCDebug(modelformat) << " offset =" << fbxgeo.offset;
qCDebug(modelformat) << " meshes.count() =" << fbxgeo.meshes.count();
foreach (FBXMesh mesh, fbxgeo.meshes) {
qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count();
qCDebug(modelformat) << " normals.count() =" << mesh.normals.count();
/*if (mesh.normals.count() == mesh.vertices.count()) {
for (int i = 0; i < mesh.normals.count(); i++) {
qCDebug(modelformat) << " " << mesh.vertices[ i ] << mesh.normals[ i ];
}
}*/
qCDebug(modelformat) << " tangents.count() =" << mesh.tangents.count();
qCDebug(modelformat) << " colors.count() =" << mesh.colors.count();
qCDebug(modelformat) << " texCoords.count() =" << mesh.texCoords.count();
qCDebug(modelformat) << " texCoords1.count() =" << mesh.texCoords1.count();
qCDebug(modelformat) << " clusterIndices.count() =" << mesh.clusterIndices.count();
qCDebug(modelformat) << " clusterWeights.count() =" << mesh.clusterWeights.count();
qCDebug(modelformat) << " meshExtents =" << mesh.meshExtents;
qCDebug(modelformat) << " modelTransform =" << mesh.modelTransform;
qCDebug(modelformat) << " parts.count() =" << mesh.parts.count();
foreach (FBXMeshPart meshPart, mesh.parts) {
qCDebug(modelformat) << " quadIndices.count() =" << meshPart.quadIndices.count();
qCDebug(modelformat) << " triangleIndices.count() =" << meshPart.triangleIndices.count();
/*
qCDebug(modelformat) << " diffuseColor =" << meshPart.diffuseColor << "mat =" << meshPart._material->getDiffuse();
qCDebug(modelformat) << " specularColor =" << meshPart.specularColor << "mat =" << meshPart._material->getMetallic();
qCDebug(modelformat) << " emissiveColor =" << meshPart.emissiveColor << "mat =" << meshPart._material->getEmissive();
qCDebug(modelformat) << " emissiveParams =" << meshPart.emissiveParams;
qCDebug(modelformat) << " gloss =" << meshPart.shininess << "mat =" << meshPart._material->getGloss();
qCDebug(modelformat) << " opacity =" << meshPart.opacity << "mat =" << meshPart._material->getOpacity();
*/
qCDebug(modelformat) << " materialID =" << meshPart.materialID;
/* qCDebug(modelformat) << " diffuse texture =" << meshPart.diffuseTexture.filename;
qCDebug(modelformat) << " specular texture =" << meshPart.specularTexture.filename;
*/
}
qCDebug(modelformat) << " clusters.count() =" << mesh.clusters.count();
foreach (FBXCluster cluster, mesh.clusters) {
qCDebug(modelformat) << " jointIndex =" << cluster.jointIndex;
qCDebug(modelformat) << " inverseBindMatrix =" << cluster.inverseBindMatrix;
}
}
qCDebug(modelformat) << " jointIndices =" << fbxgeo.jointIndices;
qCDebug(modelformat) << " joints.count() =" << fbxgeo.joints.count();
foreach (FBXJoint joint, fbxgeo.joints) {
qCDebug(modelformat) << " isFree =" << joint.isFree;
qCDebug(modelformat) << " freeLineage" << joint.freeLineage;
qCDebug(modelformat) << " parentIndex" << joint.parentIndex;
qCDebug(modelformat) << " distanceToParent" << joint.distanceToParent;
qCDebug(modelformat) << " translation" << joint.translation;
qCDebug(modelformat) << " preTransform" << joint.preTransform;
qCDebug(modelformat) << " preRotation" << joint.preRotation;
qCDebug(modelformat) << " rotation" << joint.rotation;
qCDebug(modelformat) << " postRotation" << joint.postRotation;
qCDebug(modelformat) << " postTransform" << joint.postTransform;
qCDebug(modelformat) << " transform" << joint.transform;
qCDebug(modelformat) << " rotationMin" << joint.rotationMin;
qCDebug(modelformat) << " rotationMax" << joint.rotationMax;
qCDebug(modelformat) << " inverseDefaultRotation" << joint.inverseDefaultRotation;
qCDebug(modelformat) << " inverseBindRotation" << joint.inverseBindRotation;
qCDebug(modelformat) << " bindTransform" << joint.bindTransform;
qCDebug(modelformat) << " name" << joint.name;
qCDebug(modelformat) << " isSkeletonJoint" << joint.isSkeletonJoint;
}
qCDebug(modelformat) << "\n";
}