mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-06-27 04:18:45 +02:00
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/
520 lines
16 KiB
C++
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;
|
|
}
|
|
|