overte-HifiExperiments/libraries/fbx/src/FBXReader_Node.cpp
Simon Walton 37ead11cf9 Modify FBX reader to allow for multiple empty node
Some baked files can have spurious empty nodes in
them; this was causing chunks of the FBX file to be
missed. Also more tweaks for FBX to JSON conversion.
https://highfidelity.manuscript.com/f/cases/14329/
2018-05-07 18:18:16 -07:00

520 lines
16 KiB
C++

//
// FBXReader_Node.cpp
// interface/src/fbx
//
// Created by Sam Gateau on 8/27/2015.
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "FBXReader.h"
#include <iostream>
#include <QtCore/QBuffer>
#include <QtCore/QDataStream>
#include <QtCore/QIODevice>
#include <QtCore/QStringList>
#include <QtCore/QTextStream>
#include <QtCore/QDebug>
#include <QtCore/QtEndian>
#include <QtCore/QFileInfo>
#include <shared/NsightHelpers.h>
#include "ModelFormatLogging.h"
template<class T>
int streamSize() {
return sizeof(T);
}
template<bool>
int streamSize() {
return 1;
}
template<class T>
QVariant readBinaryArray(QDataStream& in, int& position) {
quint32 arrayLength;
quint32 encoding;
quint32 compressedLength;
in >> arrayLength;
in >> encoding;
in >> compressedLength;
position += sizeof(quint32) * 3;
QVector<T> values;
if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) {
values.resize(arrayLength);
QByteArray arrayData;
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
// preface encoded data with uncompressed length
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
in.readRawData(compressed.data() + sizeof(quint32), compressedLength);
position += compressedLength;
arrayData = qUncompress(compressed);
if (arrayData.isEmpty() ||
(unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt
throw QString("corrupt fbx file");
}
} else {
arrayData.resize(sizeof(T) * arrayLength);
position += sizeof(T) * arrayLength;
in.readRawData(arrayData.data(), arrayData.size());
}
if (arrayData.size() > 0) {
memcpy(&values[0], arrayData.constData(), arrayData.size());
}
} else {
values.reserve(arrayLength);
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
// preface encoded data with uncompressed length
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
in.readRawData(compressed.data() + sizeof(quint32), compressedLength);
position += compressedLength;
QByteArray uncompressed = qUncompress(compressed);
if (uncompressed.isEmpty()) { // answers empty byte array if corrupt
throw QString("corrupt fbx file");
}
QDataStream uncompressedIn(uncompressed);
uncompressedIn.setByteOrder(QDataStream::LittleEndian);
uncompressedIn.setVersion(QDataStream::Qt_4_5); // for single/double precision switch
for (quint32 i = 0; i < arrayLength; i++) {
T value;
uncompressedIn >> value;
values.append(value);
}
} else {
for (quint32 i = 0; i < arrayLength; i++) {
T value;
in >> value;
position += streamSize<T>();
values.append(value);
}
}
}
return QVariant::fromValue(values);
}
QVariant parseBinaryFBXProperty(QDataStream& in, int& position) {
char ch;
in.device()->getChar(&ch);
position++;
switch (ch) {
case 'Y': {
qint16 value;
in >> value;
position += sizeof(qint16);
return QVariant::fromValue(value);
}
case 'C': {
bool value;
in >> value;
position++;
return QVariant::fromValue(value);
}
case 'I': {
qint32 value;
in >> value;
position += sizeof(qint32);
return QVariant::fromValue(value);
}
case 'F': {
float value;
in >> value;
position += sizeof(float);
return QVariant::fromValue(value);
}
case 'D': {
double value;
in >> value;
position += sizeof(double);
return QVariant::fromValue(value);
}
case 'L': {
qint64 value;
in >> value;
position += sizeof(qint64);
return QVariant::fromValue(value);
}
case 'f': {
return readBinaryArray<float>(in, position);
}
case 'd': {
return readBinaryArray<double>(in, position);
}
case 'l': {
return readBinaryArray<qint64>(in, position);
}
case 'i': {
return readBinaryArray<qint32>(in, position);
}
case 'b': {
return readBinaryArray<bool>(in, position);
}
case 'S':
case 'R': {
quint32 length;
in >> length;
position += sizeof(quint32) + length;
return QVariant::fromValue(in.device()->read(length));
}
default:
throw QString("Unknown property type: ") + ch;
}
}
FBXNode parseBinaryFBXNode(QDataStream& in, int& position, bool has64BitPositions = false) {
qint64 endOffset;
quint64 propertyCount;
quint64 propertyListLength;
quint8 nameLength;
// FBX 2016 and beyond uses 64bit positions in the node headers, pre-2016 used 32bit values
// our code generally doesn't care about the size that much, so we will use 64bit values
// from here on out, but if the file is an older format we read the stream into temp 32bit
// values and then assign to our actual 64bit values.
if (has64BitPositions) {
in >> endOffset;
in >> propertyCount;
in >> propertyListLength;
position += sizeof(quint64) * 3;
} else {
qint32 tempEndOffset;
quint32 tempPropertyCount;
quint32 tempPropertyListLength;
in >> tempEndOffset;
in >> tempPropertyCount;
in >> tempPropertyListLength;
position += sizeof(quint32) * 3;
endOffset = tempEndOffset;
propertyCount = tempPropertyCount;
propertyListLength = tempPropertyListLength;
}
in >> nameLength;
position += sizeof(quint8);
FBXNode node;
const int MIN_VALID_OFFSET = 40;
if (endOffset < MIN_VALID_OFFSET || nameLength == 0) {
// use a null name to indicate a null node
return node;
}
node.name = in.device()->read(nameLength);
position += nameLength;
for (quint32 i = 0; i < propertyCount; i++) {
node.properties.append(parseBinaryFBXProperty(in, position));
}
while (endOffset > position) {
FBXNode child = parseBinaryFBXNode(in, position, has64BitPositions);
if (!child.name.isNull()) {
node.children.append(child);
}
}
return node;
}
class Tokenizer {
public:
Tokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { }
enum SpecialToken {
NO_TOKEN = -1,
NO_PUSHBACKED_TOKEN = -1,
DATUM_TOKEN = 0x100
};
int nextToken();
const QByteArray& getDatum() const { return _datum; }
void pushBackToken(int token) { _pushedBackToken = token; }
void ungetChar(char ch) { _device->ungetChar(ch); }
private:
QIODevice* _device;
QByteArray _datum;
int _pushedBackToken;
};
int Tokenizer::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 ';':
_device->readLine(); // skip the comment
break;
case ':':
case '{':
case '}':
case ',':
return ch; // special punctuation
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 == ';' || ch == ':' || ch == '{' || ch == '}' || ch == ',' || ch == '\"') {
ungetChar(ch); // read until we encounter a special character, then replace it
break;
}
_datum.append(ch);
}
return DATUM_TOKEN;
}
}
return NO_TOKEN;
}
FBXNode parseTextFBXNode(Tokenizer& tokenizer) {
FBXNode node;
if (tokenizer.nextToken() != Tokenizer::DATUM_TOKEN) {
return node;
}
node.name = tokenizer.getDatum();
if (tokenizer.nextToken() != ':') {
return node;
}
int token;
bool expectingDatum = true;
while ((token = tokenizer.nextToken()) != Tokenizer::NO_TOKEN) {
if (token == '{') {
for (FBXNode child = parseTextFBXNode(tokenizer); !child.name.isNull(); child = parseTextFBXNode(tokenizer)) {
node.children.append(child);
}
return node;
}
if (token == ',') {
expectingDatum = true;
} else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) {
QByteArray datum = tokenizer.getDatum();
if ((token = tokenizer.nextToken()) == ':') {
tokenizer.ungetChar(':');
tokenizer.pushBackToken(Tokenizer::DATUM_TOKEN);
return node;
} else {
tokenizer.pushBackToken(token);
node.properties.append(datum);
expectingDatum = false;
}
} else {
tokenizer.pushBackToken(token);
return node;
}
}
return node;
}
FBXNode FBXReader::parseFBX(QIODevice* device) {
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xff0000ff, device);
// verify the prolog
if (device->peek(FBX_BINARY_PROLOG.size()) != FBX_BINARY_PROLOG) {
// parse as a text file
FBXNode top;
Tokenizer tokenizer(device);
while (device->bytesAvailable()) {
FBXNode next = parseTextFBXNode(tokenizer);
if (next.name.isNull()) {
return top;
} else {
top.children.append(next);
}
}
return top;
}
QDataStream in(device);
in.setByteOrder(QDataStream::LittleEndian);
in.setVersion(QDataStream::Qt_4_5); // for single/double precision switch
// see http://code.blender.org/index.php/2013/08/fbx-binary-file-format-specification/ for an explanation
// of the FBX binary format
// The first 27 bytes contain the header.
// Bytes 0 - 20: Kaydara FBX Binary \x00(file - magic, with 2 spaces at the end, then a NULL terminator).
// Bytes 21 - 22: [0x1A, 0x00](unknown but all observed files show these bytes).
// Bytes 23 - 26 : unsigned int, the version number. 7300 for version 7.3 for example.
in.skipRawData(FBX_HEADER_BYTES_BEFORE_VERSION);
int position = FBX_HEADER_BYTES_BEFORE_VERSION;
quint32 fileVersion;
in >> fileVersion;
position += sizeof(fileVersion);
qCDebug(modelformat) << "fileVersion:" << fileVersion;
bool has64BitPositions = (fileVersion >= FBX_VERSION_2016);
// parse the top-level node
FBXNode top;
while (device->bytesAvailable()) {
FBXNode next = parseBinaryFBXNode(in, position, has64BitPositions);
if (next.name.isNull()) {
return top;
} else {
top.children.append(next);
}
}
return top;
}
glm::vec3 FBXReader::getVec3(const QVariantList& properties, int index) {
return glm::vec3(properties.at(index).value<double>(), properties.at(index + 1).value<double>(),
properties.at(index + 2).value<double>());
}
QVector<glm::vec4> FBXReader::createVec4Vector(const QVector<double>& doubleVector) {
QVector<glm::vec4> values;
for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 4) * 4); it != end; ) {
float x = *it++;
float y = *it++;
float z = *it++;
float w = *it++;
values.append(glm::vec4(x, y, z, w));
}
return values;
}
QVector<glm::vec4> FBXReader::createVec4VectorRGBA(const QVector<double>& doubleVector, glm::vec4& average) {
QVector<glm::vec4> values;
for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 4) * 4); it != end; ) {
float x = *it++;
float y = *it++;
float z = *it++;
float w = *it++;
auto val = glm::vec4(x, y, z, w);
values.append(val);
average += val;
}
if (!values.isEmpty()) {
average *= (1.0f / float(values.size()));
}
return values;
}
QVector<glm::vec3> FBXReader::createVec3Vector(const QVector<double>& doubleVector) {
QVector<glm::vec3> values;
for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 3) * 3); it != end; ) {
float x = *it++;
float y = *it++;
float z = *it++;
values.append(glm::vec3(x, y, z));
}
return values;
}
QVector<glm::vec2> FBXReader::createVec2Vector(const QVector<double>& doubleVector) {
QVector<glm::vec2> values;
for (const double* it = doubleVector.constData(), *end = it + ((doubleVector.size() / 2) * 2); it != end; ) {
float s = *it++;
float t = *it++;
values.append(glm::vec2(s, -t));
}
return values;
}
glm::mat4 FBXReader::createMat4(const QVector<double>& doubleVector) {
return glm::mat4(doubleVector.at(0), doubleVector.at(1), doubleVector.at(2), doubleVector.at(3),
doubleVector.at(4), doubleVector.at(5), doubleVector.at(6), doubleVector.at(7),
doubleVector.at(8), doubleVector.at(9), doubleVector.at(10), doubleVector.at(11),
doubleVector.at(12), doubleVector.at(13), doubleVector.at(14), doubleVector.at(15));
}
QVector<int> FBXReader::getIntVector(const FBXNode& node) {
foreach (const FBXNode& child, node.children) {
if (child.name == "a") {
return getIntVector(child);
}
}
if (node.properties.isEmpty()) {
return QVector<int>();
}
QVector<int> vector = node.properties.at(0).value<QVector<int> >();
if (!vector.isEmpty()) {
return vector;
}
for (int i = 0; i < node.properties.size(); i++) {
vector.append(node.properties.at(i).toInt());
}
return vector;
}
QVector<float> FBXReader::getFloatVector(const FBXNode& node) {
foreach (const FBXNode& child, node.children) {
if (child.name == "a") {
return getFloatVector(child);
}
}
if (node.properties.isEmpty()) {
return QVector<float>();
}
QVector<float> vector = node.properties.at(0).value<QVector<float> >();
if (!vector.isEmpty()) {
return vector;
}
for (int i = 0; i < node.properties.size(); i++) {
vector.append(node.properties.at(i).toFloat());
}
return vector;
}
QVector<double> FBXReader::getDoubleVector(const FBXNode& node) {
foreach (const FBXNode& child, node.children) {
if (child.name == "a") {
return getDoubleVector(child);
}
}
if (node.properties.isEmpty()) {
return QVector<double>();
}
QVector<double> vector = node.properties.at(0).value<QVector<double> >();
if (!vector.isEmpty()) {
return vector;
}
for (int i = 0; i < node.properties.size(); i++) {
vector.append(node.properties.at(i).toDouble());
}
return vector;
}