Merge branch 'master' of https://github.com/highfidelity/hifi into workload

This commit is contained in:
samcake 2018-03-16 15:35:17 -07:00
commit fb7753ebaa
33 changed files with 1331 additions and 681 deletions

View file

@ -14,9 +14,9 @@ import Qt.labs.settings 1.0
import "./hifi/audio" as HifiAudio
Hifi.AvatarInputs {
Item {
id: root;
objectName: "AvatarInputs"
objectName: "AvatarInputsBar"
property int modality: Qt.NonModal
width: audio.width;
height: audio.height;
@ -26,7 +26,7 @@ Hifi.AvatarInputs {
HifiAudio.MicBar {
id: audio;
visible: root.showAudioTools;
visible: AvatarInputs.showAudioTools;
standalone: true;
dragTarget: parent;
}

View file

@ -2722,10 +2722,12 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
void Application::onDesktopRootItemCreated(QQuickItem* rootItem) {
Stats::show();
AvatarInputs::show();
auto surfaceContext = DependencyManager::get<OffscreenUi>()->getSurfaceContext();
surfaceContext->setContextProperty("Stats", Stats::getInstance());
surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance());
auto offscreenUi = DependencyManager::get<OffscreenUi>();
auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml");
offscreenUi->show(qml, "AvatarInputsBar");
}
void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) {

View file

@ -16,19 +16,19 @@
#include "Application.h"
#include "Menu.h"
HIFI_QML_DEF(AvatarInputs)
static AvatarInputs* INSTANCE{ nullptr };
Setting::Handle<bool> showAudioToolsSetting { QStringList { "AvatarInputs", "showAudioTools" }, false };
AvatarInputs* AvatarInputs::getInstance() {
Q_ASSERT(INSTANCE);
if (!INSTANCE) {
INSTANCE = new AvatarInputs();
Q_ASSERT(INSTANCE);
}
return INSTANCE;
}
AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) {
INSTANCE = this;
AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) {
_showAudioTools = showAudioToolsSetting.get();
}

View file

@ -19,7 +19,7 @@ public: \
private: \
type _##name{ initialValue };
class AvatarInputs : public QQuickItem {
class AvatarInputs : public QObject {
Q_OBJECT
HIFI_QML_DECL
@ -32,7 +32,7 @@ class AvatarInputs : public QQuickItem {
public:
static AvatarInputs* getInstance();
Q_INVOKABLE float loudnessToAudioLevel(float loudness);
AvatarInputs(QQuickItem* parent = nullptr);
AvatarInputs(QObject* parent = nullptr);
void update();
bool showAudioTools() const { return _showAudioTools; }

View file

@ -37,7 +37,8 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
_ignoreRayIntersection(base3DOverlay->_ignoreRayIntersection),
_drawInFront(base3DOverlay->_drawInFront),
_drawHUDLayer(base3DOverlay->_drawHUDLayer),
_isGrabbable(base3DOverlay->_isGrabbable)
_isGrabbable(base3DOverlay->_isGrabbable),
_isVisibleInSecondaryCamera(base3DOverlay->_isVisibleInSecondaryCamera)
{
setTransform(base3DOverlay->getTransform());
}
@ -142,6 +143,13 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
setIsGrabbable(isGrabbable.toBool());
}
auto isVisibleInSecondaryCamera = properties["isVisibleInSecondaryCamera"];
if (isVisibleInSecondaryCamera.isValid()) {
bool value = isVisibleInSecondaryCamera.toBool();
setIsVisibleInSecondaryCamera(value);
needRenderItemUpdate = true;
}
if (properties["position"].isValid()) {
setLocalPosition(vec3FromVariant(properties["position"]));
needRenderItemUpdate = true;
@ -221,6 +229,8 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
* @property {boolean} drawInFront=false - If <code>true</code>, the overlay is rendered in front of other overlays that don't
* have <code>drawInFront</code> set to <code>true</code>, and in front of entities.
* @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed.
* @property {boolean} isVisibleInSecondaryCamera=false - If <code>true</code>, the overlay is rendered in secondary
* camera views.
* @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to.
* @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if
* <code>parentID</code> is an avatar skeleton. A value of <code>65535</code> means "no joint".
@ -259,6 +269,9 @@ QVariant Base3DOverlay::getProperty(const QString& property) {
if (property == "grabbable") {
return _isGrabbable;
}
if (property == "isVisibleInSecondaryCamera") {
return _isVisibleInSecondaryCamera;
}
if (property == "parentID") {
return getParentID();
}

View file

@ -48,6 +48,7 @@ public:
bool getDrawInFront() const { return _drawInFront; }
bool getDrawHUDLayer() const { return _drawHUDLayer; }
bool getIsGrabbable() const { return _isGrabbable; }
virtual bool getIsVisibleInSecondaryCamera() const override { return _isVisibleInSecondaryCamera; }
void setIsSolid(bool isSolid) { _isSolid = isSolid; }
void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; }
@ -55,6 +56,7 @@ public:
virtual void setDrawInFront(bool value) { _drawInFront = value; }
virtual void setDrawHUDLayer(bool value) { _drawHUDLayer = value; }
void setIsGrabbable(bool value) { _isGrabbable = value; }
virtual void setIsVisibleInSecondaryCamera(bool value) { _isVisibleInSecondaryCamera = value; }
virtual AABox getBounds() const override = 0;
@ -92,6 +94,7 @@ protected:
bool _drawInFront;
bool _drawHUDLayer;
bool _isGrabbable { false };
bool _isVisibleInSecondaryCamera { false };
mutable bool _renderVariableDirty { true };
QString _name;

View file

@ -89,8 +89,11 @@ void ModelOverlay::update(float deltatime) {
}
if (_visibleDirty) {
_visibleDirty = false;
// don't show overlays in mirrors
_model->setVisibleInScene(getVisible(), scene, render::ItemKey::TAG_BITS_0, false);
// don't show overlays in mirrors or spectator-cam unless _isVisibleInSecondaryCamera is true
_model->setVisibleInScene(getVisible(), scene,
render::ItemKey::TAG_BITS_0 |
(_isVisibleInSecondaryCamera ? render::ItemKey::TAG_BITS_1 : render::ItemKey::TAG_BITS_NONE),
false);
}
if (_drawInFrontDirty) {
_drawInFrontDirty = false;

View file

@ -36,6 +36,11 @@ public:
void clearSubRenderItemIDs();
void setSubRenderItemIDs(const render::ItemIDs& ids);
virtual void setIsVisibleInSecondaryCamera(bool value) override {
Base3DOverlay::setIsVisibleInSecondaryCamera(value);
_visibleDirty = true;
}
void setProperties(const QVariantMap& properties) override;
QVariant getProperty(const QString& property) override;
virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance,

View file

@ -56,6 +56,8 @@ public:
bool isLoaded() { return _isLoaded; }
bool getVisible() const { return _visible; }
virtual bool isTransparent() { return getAlphaPulse() != 0.0f || getAlpha() != 1.0f; };
virtual bool getIsVisibleInSecondaryCamera() const { return false; }
xColor getColor();
float getAlpha();

View file

@ -49,7 +49,11 @@ namespace render {
builder.withInvisible();
}
builder.withTagBits(render::ItemKey::TAG_BITS_0); // Only draw overlays in main view
// always visible in primary view. if isVisibleInSecondaryCamera, also draw in secondary view
uint32_t viewTaskBits = render::ItemKey::TAG_BITS_0 |
(overlay->getIsVisibleInSecondaryCamera() ? render::ItemKey::TAG_BITS_1 : render::ItemKey::TAG_BITS_NONE);
builder.withTagBits(viewTaskBits);
return builder.build();
}

View file

@ -66,16 +66,12 @@ bool ElbowConstraint::apply(glm::quat& rotation) const {
bool twistWasClamped = (twistAngle != clampedTwistAngle);
// update rotation
const float MIN_SWING_REAL_PART = 0.99999f;
if (twistWasClamped || fabsf(swingRotation.w) < MIN_SWING_REAL_PART) {
if (twistWasClamped) {
twistRotation = glm::angleAxis(clampedTwistAngle, _axis);
}
// we discard all swing and only keep twist
rotation = twistRotation * _referenceRotation;
return true;
if (twistWasClamped) {
twistRotation = glm::angleAxis(clampedTwistAngle, _axis);
}
return false;
// we discard all swing and only keep twist
rotation = twistRotation * _referenceRotation;
return true;
}
glm::quat ElbowConstraint::computeCenterRotation() const {

View file

@ -33,65 +33,8 @@
#include "FBXBaker.h"
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
#include <draco/mesh/triangle_soup_mesh_builder.h>
#include <draco/compression/encode.h>
#ifdef _WIN32
#pragma warning( pop )
#endif
FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
const QString& bakedOutputDir, const QString& originalOutputDir) :
_fbxURL(fbxURL),
_bakedOutputDir(bakedOutputDir),
_originalOutputDir(originalOutputDir),
_textureThreadGetter(textureThreadGetter)
{
}
FBXBaker::~FBXBaker() {
if (_tempDir.exists()) {
if (!_tempDir.remove(_originalFBXFilePath)) {
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalFBXFilePath;
}
if (!_tempDir.rmdir(".")) {
qCWarning(model_baking) << "Failed to remove temporary directory:" << _tempDir;
}
}
}
void FBXBaker::abort() {
Baker::abort();
// tell our underlying TextureBaker instances to abort
// the FBXBaker will wait until all are aborted before emitting its own abort signal
for (auto& textureBaker : _bakingTextures) {
textureBaker->abort();
}
}
void FBXBaker::bake() {
qDebug() << "FBXBaker" << _fbxURL << "bake starting";
auto tempDir = PathUtils::generateTemporaryDir();
if (tempDir.isEmpty()) {
handleError("Failed to create a temporary directory.");
return;
}
_tempDir = tempDir;
_originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName());
qDebug() << "Made temporary dir " << _tempDir;
qDebug() << "Origin file path: " << _originalFBXFilePath;
void FBXBaker::bake() {
qDebug() << "FBXBaker" << _modelURL << "bake starting";
// setup the output folder for the results of this bake
setupOutputFolder();
@ -152,7 +95,7 @@ void FBXBaker::setupOutputFolder() {
}
// attempt to make the output folder
if (!QDir().mkpath(_originalOutputDir)) {
handleError("Failed to create FBX output folder " + _bakedOutputDir);
handleError("Failed to create FBX output folder " + _originalOutputDir);
return;
}
}
@ -160,25 +103,25 @@ void FBXBaker::setupOutputFolder() {
void FBXBaker::loadSourceFBX() {
// check if the FBX is local or first needs to be downloaded
if (_fbxURL.isLocalFile()) {
if (_modelURL.isLocalFile()) {
// load up the local file
QFile localFBX { _fbxURL.toLocalFile() };
QFile localFBX { _modelURL.toLocalFile() };
qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath;
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath;
if (!localFBX.exists()) {
//QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), "");
handleError("Could not find " + _fbxURL.toString());
handleError("Could not find " + _modelURL.toString());
return;
}
// make a copy in the output folder
if (!_originalOutputDir.isEmpty()) {
qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName();
localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName());
qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName();
localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
localFBX.copy(_originalFBXFilePath);
localFBX.copy(_originalModelFilePath);
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
@ -193,9 +136,9 @@ void FBXBaker::loadSourceFBX() {
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_fbxURL);
networkRequest.setUrl(_modelURL);
qCDebug(model_baking) << "Downloading" << _fbxURL;
qCDebug(model_baking) << "Downloading" << _modelURL;
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply);
@ -206,20 +149,20 @@ void FBXBaker::handleFBXNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded" << _fbxURL;
qCDebug(model_baking) << "Downloaded" << _modelURL;
// grab the contents of the reply and make a copy in the output folder
QFile copyOfOriginal(_originalFBXFilePath);
QFile copyOfOriginal(_originalModelFilePath);
qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName();
qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
// add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to open " + _originalFBXFilePath + ")");
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")");
return;
}
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)");
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)");
return;
}
@ -227,98 +170,32 @@ void FBXBaker::handleFBXNetworkReply() {
copyOfOriginal.close();
if (!_originalOutputDir.isEmpty()) {
copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName());
copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
} else {
// add an error to our list stating that the FBX could not be downloaded
handleError("Failed to download " + _fbxURL.toString());
handleError("Failed to download " + _modelURL.toString());
}
}
void FBXBaker::importScene() {
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists();
QFile fbxFile(_originalFBXFilePath);
QFile fbxFile(_originalModelFilePath);
if (!fbxFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalFBXFilePath + " for reading");
handleError("Error opening " + _originalModelFilePath + " for reading");
return;
}
FBXReader reader;
qCDebug(model_baking) << "Parsing" << _fbxURL;
qCDebug(model_baking) << "Parsing" << _modelURL;
_rootNode = reader._rootNode = reader.parseFBX(&fbxFile);
_geometry = reader.extractFBXGeometry({}, _fbxURL.toString());
_textureContent = reader._textureContent;
}
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
if (texturePath.startsWith(fbxPath)) {
// texture path is a child of the FBX path, return the texture path without the fbx path
return texturePath.mid(fbxPath.length());
} else {
// the texture path was not a child of the FBX path, return the empty string
return "";
}
}
QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
// first make sure we have a unique base name for this texture
// in case another texture referenced by this model has the same base name
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
QString bakedTextureFileName { textureFileInfo.completeBaseName() };
if (nameMatches > 0) {
// there are already nameMatches texture with this name
// append - and that number to our baked texture file name so that it is unique
bakedTextureFileName += "-" + QString::number(nameMatches);
}
bakedTextureFileName += BAKED_TEXTURE_EXT;
// increment the number of name matches
++nameMatches;
return bakedTextureFileName;
}
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) {
QUrl urlToTexture;
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
if (isEmbedded) {
urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath();
} else {
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
// set the texture URL to the local texture that we have confirmed exists
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
} else {
// external texture that we'll need to download or find
// this is a relative file path which will require different handling
// depending on the location of the original FBX
if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
// the absolute path we ran into for the texture in the FBX exists on this machine
// so use that file
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
} else {
// we didn't find the texture on this machine at the absolute path
// so assume that it is right beside the FBX to match the behaviour of interface
urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName());
}
}
}
return urlToTexture;
_geometry = reader.extractFBXGeometry({}, _modelURL.toString());
_textureContentMap = reader._textureContent;
}
void FBXBaker::rewriteAndBakeSceneModels() {
@ -344,176 +221,25 @@ void FBXBaker::rewriteAndBakeSceneModels() {
// TODO Pull this out of _geometry instead so we don't have to reprocess it
auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false);
auto& mesh = extractedMesh.mesh;
if (mesh.wasCompressed) {
handleError("Cannot re-bake a file that contains compressed mesh");
return;
}
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
int64_t numTriangles { 0 };
for (auto& part : mesh.parts) {
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
handleWarning("Found a mesh part with invalid index data, skipping");
// Callback to get MaterialID
GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) {
return extractedMesh.partMaterialTextures[partIndex].first;
};
// Compress mesh information and store in dracoMeshNode
FBXNode dracoMeshNode;
bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback);
// if bake fails - return, if there were errors and continue, if there were warnings.
if (!success) {
if (hasErrors()) {
return;
} else if (hasWarnings()) {
continue;
}
numTriangles += part.quadTrianglesIndices.size() / 3;
numTriangles += part.triangleIndices.size() / 3;
}
if (numTriangles == 0) {
continue;
}
draco::TriangleSoupMeshBuilder meshBuilder;
meshBuilder.Start(numTriangles);
bool hasNormals { mesh.normals.size() > 0 };
bool hasColors { mesh.colors.size() > 0 };
bool hasTexCoords { mesh.texCoords.size() > 0 };
bool hasTexCoords1 { mesh.texCoords1.size() > 0 };
bool hasPerFaceMaterials { mesh.parts.size() > 1
|| extractedMesh.partMaterialTextures[0].first != 0 };
bool needsOriginalIndices { hasDeformers };
int normalsAttributeID { -1 };
int colorsAttributeID { -1 };
int texCoordsAttributeID { -1 };
int texCoords1AttributeID { -1 };
int faceMaterialAttributeID { -1 };
int originalIndexAttributeID { -1 };
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
3, draco::DT_FLOAT32);
if (needsOriginalIndices) {
originalIndexAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
1, draco::DT_INT32);
}
if (hasNormals) {
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
3, draco::DT_FLOAT32);
}
if (hasColors) {
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
3, draco::DT_FLOAT32);
}
if (hasTexCoords) {
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
2, draco::DT_FLOAT32);
}
if (hasTexCoords1) {
texCoords1AttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
2, draco::DT_FLOAT32);
}
if (hasPerFaceMaterials) {
faceMaterialAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
1, draco::DT_UINT16);
}
auto partIndex = 0;
draco::FaceIndex face;
for (auto& part : mesh.parts) {
const auto& matTex = extractedMesh.partMaterialTextures[partIndex];
uint16_t materialID = matTex.first;
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
int32_t idx0 = indices[index];
int32_t idx1 = indices[index + 1];
int32_t idx2 = indices[index + 2];
if (hasPerFaceMaterials) {
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
}
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
&mesh.vertices[idx0], &mesh.vertices[idx1],
&mesh.vertices[idx2]);
if (needsOriginalIndices) {
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
&mesh.originalIndices[idx0],
&mesh.originalIndices[idx1],
&mesh.originalIndices[idx2]);
}
if (hasNormals) {
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
&mesh.normals[idx0], &mesh.normals[idx1],
&mesh.normals[idx2]);
}
if (hasColors) {
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
&mesh.colors[idx0], &mesh.colors[idx1],
&mesh.colors[idx2]);
}
if (hasTexCoords) {
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
&mesh.texCoords[idx2]);
}
if (hasTexCoords1) {
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
&mesh.texCoords1[idx2]);
}
};
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
addFace(part.quadTrianglesIndices, i, face++);
}
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
addFace(part.triangleIndices, i, face++);
}
partIndex++;
}
auto dracoMesh = meshBuilder.Finalize();
if (!dracoMesh) {
handleWarning("Failed to finalize the baking of a draco Geometry node");
continue;
}
// we need to modify unique attribute IDs for custom attributes
// so the attributes are easily retrievable on the other side
if (hasPerFaceMaterials) {
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
}
if (hasTexCoords1) {
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
}
if (needsOriginalIndices) {
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
draco::Encoder encoder;
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
encoder.SetSpeedOptions(0, 5);
draco::EncoderBuffer buffer;
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
FBXNode dracoMeshNode;
dracoMeshNode.name = "DracoMesh";
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int) buffer.size()));
dracoMeshNode.properties.append(value);
objectChild.children.push_back(dracoMeshNode);
static const std::vector<QString> nodeNamesToDelete {
@ -590,69 +316,25 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
for (FBXNode& textureChild : object->children) {
if (textureChild.name == "RelativeFilename") {
QString fbxTextureFileName { textureChild.properties.at(0).toString() };
// grab the ID for this texture so we can figure out the
// texture type from the loaded materials
auto textureID { object->properties[0].toString() };
auto textureType = textureTypes[textureID];
// use QFileInfo to easily split up the existing texture filename into its components
QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() };
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
// Compress the texture information and return the new filename to be added into the FBX scene
auto bakedTextureFile = compressTexture(fbxTextureFileName, textureType);
if (hasErrors()) {
return;
}
if (textureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
// re-baking an FBX that already references baked textures is a fail
// so we add an error and return from here
handleError("Cannot re-bake a file that references compressed textures");
return;
}
if (!image::getSupportedFormats().contains(textureFileInfo.suffix())) {
// this is a texture format we don't bake, skip it
handleWarning(fbxTextureFileName + " is not a bakeable texture format");
continue;
}
// make sure this texture points to something and isn't one we've already re-mapped
if (!textureFileInfo.filePath().isEmpty()) {
// check if this was an embedded texture we have already have in-memory content for
auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
// figure out the URL to this texture, embedded or external
auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName,
!textureContent.isNull());
QString bakedTextureFileName;
if (_remappedTexturePaths.contains(urlToTexture)) {
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
} else {
// construct the new baked texture file name and file path
// ensuring that the baked texture will have a unique name
// even if there was another texture with the same name at a different path
bakedTextureFileName = createBakedTextureFileName(textureFileInfo);
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
}
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
<< "to" << bakedTextureFileName;
QString bakedTextureFilePath {
_bakedOutputDir + "/" + bakedTextureFileName
};
// write the new filename into the FBX scene
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
if (!_bakingTextures.contains(urlToTexture)) {
_outputFiles.push_back(bakedTextureFilePath);
// grab the ID for this texture so we can figure out the
// texture type from the loaded materials
QString textureID { object->properties[0].toByteArray() };
auto textureType = textureTypes[textureID];
// bake this texture asynchronously
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
// If no errors or warnings have occurred during texture compression add the filename to the FBX scene
if (!bakedTextureFile.isNull()) {
textureChild.properties[0] = bakedTextureFile;
} else {
// if bake fails - return, if there were errors and continue, if there were warnings.
if (hasErrors()) {
return;
} else if (hasWarnings()) {
continue;
}
}
}
@ -671,172 +353,26 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
}
}
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
// start a bake for this texture and add it to our list to keep track of
QSharedPointer<TextureBaker> bakingTexture {
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
&TextureBaker::deleteLater
};
// make sure we hear when the baking texture is done or aborted
connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture);
connect(bakingTexture.data(), &TextureBaker::aborted, this, &FBXBaker::handleAbortedTexture);
// keep a shared pointer to the baking texture
_bakingTextures.insert(textureURL, bakingTexture);
// start baking the texture on one of our available worker threads
bakingTexture->moveToThread(_textureThreadGetter());
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
}
void FBXBaker::handleBakedTexture() {
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
// make sure we haven't already run into errors, and that this is a valid texture
if (bakedTexture) {
if (!shouldStop()) {
if (!bakedTexture->hasErrors()) {
if (!_originalOutputDir.isEmpty()) {
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
// use the path to the texture being baked to determine if this was an embedded or a linked texture
// it is embeddded if the texure being baked was inside a folder with the name of the FBX
// since that is the fake URL we provide when baking external textures
if (!_fbxURL.isParentOf(bakedTexture->getTextureURL())) {
// for linked textures we want to save a copy of original texture beside the original FBX
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
// check if we have a relative path to use for the texture
auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL());
QFile originalTextureFile {
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
};
if (relativeTexturePath.length() > 0) {
// make the folders needed by the relative path
}
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
<< "for" << _fbxURL;
} else {
handleError("Could not save original external texture " + originalTextureFile.fileName()
+ " for " + _fbxURL.toString());
return;
}
}
}
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
} else {
// there was an error baking this texture - add it to our list of errors
_errorList.append(bakedTexture->getErrors());
// we don't emit finished yet so that the other textures can finish baking first
_pendingErrorEmission = true;
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
_bakingTextures.remove(bakedTexture->getTextureURL());
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
} else {
// we have errors to attend to, so we don't do extra processing for this texture
// but we do need to remove that TextureBaker from our list
// and then check if we're done with all textures
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
}
}
}
void FBXBaker::handleAbortedTexture() {
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
if (bakedTexture) {
_bakingTextures.remove(bakedTexture->getTextureURL());
}
// since a texture we were baking aborted, our status is also aborted
_shouldAbort.store(true);
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
void FBXBaker::exportScene() {
// save the relative path to this FBX inside our passed output folder
auto fileName = _fbxURL.fileName();
auto fileName = _modelURL.fileName();
auto baseName = fileName.left(fileName.lastIndexOf('.'));
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
_bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename;
_bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename;
auto fbxData = FBXWriter::encodeFBX(_rootNode);
QFile bakedFile(_bakedFBXFilePath);
QFile bakedFile(_bakedModelFilePath);
if (!bakedFile.open(QIODevice::WriteOnly)) {
handleError("Error opening " + _bakedFBXFilePath + " for writing");
handleError("Error opening " + _bakedModelFilePath + " for writing");
return;
}
bakedFile.write(fbxData);
_outputFiles.push_back(_bakedFBXFilePath);
_outputFiles.push_back(_bakedModelFilePath);
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
}
void FBXBaker::checkIfTexturesFinished() {
// check if we're done everything we need to do for this FBX
// and emit our finished signal if we're done
if (_bakingTextures.isEmpty()) {
if (shouldStop()) {
// if we're checking for completion but we have errors
// that means one or more of our texture baking operations failed
if (_pendingErrorEmission) {
setIsFinished(true);
}
return;
} else {
qCDebug(model_baking) << "Finished baking, emitting finsihed" << _fbxURL;
setIsFinished(true);
}
}
}
void FBXBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
if (wasAborted) {
qCDebug(model_baking) << "Aborted baking" << _fbxURL;
}
}
qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath;
}

View file

@ -19,7 +19,7 @@
#include "Baker.h"
#include "TextureBaker.h"
#include "ModelBaker.h"
#include "ModelBakingLoggingCategory.h"
#include <gpu/Texture.h>
@ -30,21 +30,13 @@ static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
using TextureBakerThreadGetter = std::function<QThread*()>;
class FBXBaker : public Baker {
class FBXBaker : public ModelBaker {
Q_OBJECT
public:
FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
const QString& bakedOutputDir, const QString& originalOutputDir = "");
~FBXBaker() override;
QUrl getFBXUrl() const { return _fbxURL; }
QString getBakedFBXFilePath() const { return _bakedFBXFilePath; }
virtual void setWasAborted(bool wasAborted) override;
using ModelBaker::ModelBaker;
public slots:
virtual void bake() override;
virtual void abort() override;
signals:
void sourceCopyReadyToLoad();
@ -52,8 +44,6 @@ signals:
private slots:
void bakeSourceCopy();
void handleFBXNetworkReply();
void handleBakedTexture();
void handleAbortedTexture();
private:
void setupOutputFolder();
@ -64,38 +54,12 @@ private:
void rewriteAndBakeSceneModels();
void rewriteAndBakeSceneTextures();
void exportScene();
void removeEmbeddedMediaFolder();
void checkIfTexturesFinished();
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir,
const QString& bakedFilename, const QByteArray& textureContent = QByteArray());
QUrl _fbxURL;
FBXNode _rootNode;
FBXGeometry* _geometry;
QHash<QByteArray, QByteArray> _textureContent;
QString _bakedFBXFilePath;
QString _bakedOutputDir;
// If set, the original FBX and textures will also be copied here
QString _originalOutputDir;
QDir _tempDir;
QString _originalFBXFilePath;
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
QHash<QUrl, QString> _remappedTexturePaths;
TextureBakerThreadGetter _textureThreadGetter;
bool _pendingErrorEmission { false };
};

View file

@ -0,0 +1,521 @@
//
// ModelBaker.cpp
// libraries/baking/src
//
// Created by Utkarsh Gautam on 9/29/17.
// Copyright 2017 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 "ModelBaker.h"
#include <PathUtils.h>
#include <FBXReader.h>
#include <FBXWriter.h>
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
#include <draco/mesh/triangle_soup_mesh_builder.h>
#include <draco/compression/encode.h>
#ifdef _WIN32
#pragma warning( pop )
#endif
ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory) :
_modelURL(inputModelURL),
_bakedOutputDir(bakedOutputDirectory),
_originalOutputDir(originalOutputDirectory),
_textureThreadGetter(inputTextureThreadGetter)
{
auto tempDir = PathUtils::generateTemporaryDir();
if (tempDir.isEmpty()) {
handleError("Failed to create a temporary directory.");
return;
}
_modelTempDir = tempDir;
_originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName());
qDebug() << "Made temporary dir " << _modelTempDir;
qDebug() << "Origin file path: " << _originalModelFilePath;
}
ModelBaker::~ModelBaker() {
if (_modelTempDir.exists()) {
if (!_modelTempDir.remove(_originalModelFilePath)) {
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath;
}
if (!_modelTempDir.rmdir(".")) {
qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir;
}
}
}
void ModelBaker::abort() {
Baker::abort();
// tell our underlying TextureBaker instances to abort
// the ModelBaker will wait until all are aborted before emitting its own abort signal
for (auto& textureBaker : _bakingTextures) {
textureBaker->abort();
}
}
bool ModelBaker::compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) {
if (mesh.wasCompressed) {
handleError("Cannot re-bake a file that contains compressed mesh");
return false;
}
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
int64_t numTriangles{ 0 };
for (auto& part : mesh.parts) {
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
handleWarning("Found a mesh part with invalid index data, skipping");
continue;
}
numTriangles += part.quadTrianglesIndices.size() / 3;
numTriangles += part.triangleIndices.size() / 3;
}
if (numTriangles == 0) {
return false;
}
draco::TriangleSoupMeshBuilder meshBuilder;
meshBuilder.Start(numTriangles);
bool hasNormals{ mesh.normals.size() > 0 };
bool hasColors{ mesh.colors.size() > 0 };
bool hasTexCoords{ mesh.texCoords.size() > 0 };
bool hasTexCoords1{ mesh.texCoords1.size() > 0 };
bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true;
bool needsOriginalIndices{ hasDeformers };
int normalsAttributeID { -1 };
int colorsAttributeID { -1 };
int texCoordsAttributeID { -1 };
int texCoords1AttributeID { -1 };
int faceMaterialAttributeID { -1 };
int originalIndexAttributeID { -1 };
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
3, draco::DT_FLOAT32);
if (needsOriginalIndices) {
originalIndexAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
1, draco::DT_INT32);
}
if (hasNormals) {
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
3, draco::DT_FLOAT32);
}
if (hasColors) {
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
3, draco::DT_FLOAT32);
}
if (hasTexCoords) {
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
2, draco::DT_FLOAT32);
}
if (hasTexCoords1) {
texCoords1AttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
2, draco::DT_FLOAT32);
}
if (hasPerFaceMaterials) {
faceMaterialAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
1, draco::DT_UINT16);
}
auto partIndex = 0;
draco::FaceIndex face;
uint16_t materialID;
for (auto& part : mesh.parts) {
materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex;
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
int32_t idx0 = indices[index];
int32_t idx1 = indices[index + 1];
int32_t idx2 = indices[index + 2];
if (hasPerFaceMaterials) {
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
}
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
&mesh.vertices[idx0], &mesh.vertices[idx1],
&mesh.vertices[idx2]);
if (needsOriginalIndices) {
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
&mesh.originalIndices[idx0],
&mesh.originalIndices[idx1],
&mesh.originalIndices[idx2]);
}
if (hasNormals) {
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
&mesh.normals[idx0], &mesh.normals[idx1],
&mesh.normals[idx2]);
}
if (hasColors) {
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
&mesh.colors[idx0], &mesh.colors[idx1],
&mesh.colors[idx2]);
}
if (hasTexCoords) {
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
&mesh.texCoords[idx2]);
}
if (hasTexCoords1) {
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
&mesh.texCoords1[idx2]);
}
};
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
addFace(part.quadTrianglesIndices, i, face++);
}
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
addFace(part.triangleIndices, i, face++);
}
partIndex++;
}
auto dracoMesh = meshBuilder.Finalize();
if (!dracoMesh) {
handleWarning("Failed to finalize the baking of a draco Geometry node");
return false;
}
// we need to modify unique attribute IDs for custom attributes
// so the attributes are easily retrievable on the other side
if (hasPerFaceMaterials) {
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
}
if (hasTexCoords1) {
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
}
if (needsOriginalIndices) {
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
draco::Encoder encoder;
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
encoder.SetSpeedOptions(0, 5);
draco::EncoderBuffer buffer;
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
FBXNode dracoNode;
dracoNode.name = "DracoMesh";
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size()));
dracoNode.properties.append(value);
dracoMeshNode = dracoNode;
// Mesh compression successful return true
return true;
}
QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) {
QFileInfo modelTextureFileInfo{ modelTextureFileName.replace("\\", "/") };
if (modelTextureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
// re-baking a model that already references baked textures
// this is an error - return from here
handleError("Cannot re-bake a file that already references compressed textures");
return QString::null;
}
if (!image::getSupportedFormats().contains(modelTextureFileInfo.suffix())) {
// this is a texture format we don't bake, skip it
handleWarning(modelTextureFileName + " is not a bakeable texture format");
return QString::null;
}
// make sure this texture points to something and isn't one we've already re-mapped
QString textureChild { QString::null };
if (!modelTextureFileInfo.filePath().isEmpty()) {
// check if this was an embedded texture that we already have in-memory content for
QByteArray textureContent;
// figure out the URL to this texture, embedded or external
if (!modelTextureFileInfo.filePath().isEmpty()) {
textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit());
}
auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull());
QString bakedTextureFileName;
if (_remappedTexturePaths.contains(urlToTexture)) {
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
} else {
// construct the new baked texture file name and file path
// ensuring that the baked texture will have a unique name
// even if there was another texture with the same name at a different path
bakedTextureFileName = createBakedTextureFileName(modelTextureFileInfo);
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
}
qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName
<< "to" << bakedTextureFileName;
QString bakedTextureFilePath{
_bakedOutputDir + "/" + bakedTextureFileName
};
textureChild = bakedTextureFileName;
if (!_bakingTextures.contains(urlToTexture)) {
_outputFiles.push_back(bakedTextureFilePath);
// bake this texture asynchronously
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
}
}
return textureChild;
}
void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
// start a bake for this texture and add it to our list to keep track of
QSharedPointer<TextureBaker> bakingTexture{
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
&TextureBaker::deleteLater
};
// make sure we hear when the baking texture is done or aborted
connect(bakingTexture.data(), &Baker::finished, this, &ModelBaker::handleBakedTexture);
connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture);
// keep a shared pointer to the baking texture
_bakingTextures.insert(textureURL, bakingTexture);
// start baking the texture on one of our available worker threads
bakingTexture->moveToThread(_textureThreadGetter());
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
}
void ModelBaker::handleBakedTexture() {
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
qDebug() << "Handling baked texture" << bakedTexture->getTextureURL();
// make sure we haven't already run into errors, and that this is a valid texture
if (bakedTexture) {
if (!shouldStop()) {
if (!bakedTexture->hasErrors()) {
if (!_originalOutputDir.isEmpty()) {
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
// use the path to the texture being baked to determine if this was an embedded or a linked texture
// it is embeddded if the texure being baked was inside a folder with the name of the model
// since that is the fake URL we provide when baking external textures
if (!_modelURL.isParentOf(bakedTexture->getTextureURL())) {
// for linked textures we want to save a copy of original texture beside the original model
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
// check if we have a relative path to use for the texture
auto relativeTexturePath = texturePathRelativeToModel(_modelURL, bakedTexture->getTextureURL());
QFile originalTextureFile{
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
};
if (relativeTexturePath.length() > 0) {
// make the folders needed by the relative path
}
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
<< "for" << _modelURL;
} else {
handleError("Could not save original external texture " + originalTextureFile.fileName()
+ " for " + _modelURL.toString());
return;
}
}
}
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
} else {
// there was an error baking this texture - add it to our list of errors
_errorList.append(bakedTexture->getErrors());
// we don't emit finished yet so that the other textures can finish baking first
_pendingErrorEmission = true;
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
_bakingTextures.remove(bakedTexture->getTextureURL());
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
} else {
// we have errors to attend to, so we don't do extra processing for this texture
// but we do need to remove that TextureBaker from our list
// and then check if we're done with all textures
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
}
}
}
void ModelBaker::handleAbortedTexture() {
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
qDebug() << "Texture aborted: " << bakedTexture->getTextureURL();
if (bakedTexture) {
_bakingTextures.remove(bakedTexture->getTextureURL());
}
// since a texture we were baking aborted, our status is also aborted
_shouldAbort.store(true);
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) {
QUrl urlToTexture;
// use QFileInfo to easily split up the existing texture filename into its components
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
if (isEmbedded) {
urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath();
} else {
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
// set the texture URL to the local texture that we have confirmed exists
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
} else {
// external texture that we'll need to download or find
// this is a relative file path which will require different handling
// depending on the location of the original model
if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
// the absolute path we ran into for the texture in the model exists on this machine
// so use that file
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
} else {
// we didn't find the texture on this machine at the absolute path
// so assume that it is right beside the model to match the behaviour of interface
urlToTexture = _modelURL.resolved(apparentRelativePath.fileName());
}
}
}
return urlToTexture;
}
QString ModelBaker::texturePathRelativeToModel(QUrl modelURL, QUrl textureURL) {
auto modelPath = modelURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
if (texturePath.startsWith(modelPath)) {
// texture path is a child of the model path, return the texture path without the model path
return texturePath.mid(modelPath.length());
} else {
// the texture path was not a child of the model path, return the empty string
return "";
}
}
void ModelBaker::checkIfTexturesFinished() {
// check if we're done everything we need to do for this model
// and emit our finished signal if we're done
if (_bakingTextures.isEmpty()) {
if (shouldStop()) {
// if we're checking for completion but we have errors
// that means one or more of our texture baking operations failed
if (_pendingErrorEmission) {
setIsFinished(true);
}
return;
} else {
qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL;
setIsFinished(true);
}
}
}
QString ModelBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
// first make sure we have a unique base name for this texture
// in case another texture referenced by this model has the same base name
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
QString bakedTextureFileName{ textureFileInfo.completeBaseName() };
if (nameMatches > 0) {
// there are already nameMatches texture with this name
// append - and that number to our baked texture file name so that it is unique
bakedTextureFileName += "-" + QString::number(nameMatches);
}
bakedTextureFileName += BAKED_TEXTURE_EXT;
// increment the number of name matches
++nameMatches;
return bakedTextureFileName;
}
void ModelBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
if (wasAborted) {
qCDebug(model_baking) << "Aborted baking" << _modelURL;
}
}
}

View file

@ -0,0 +1,79 @@
//
// ModelBaker.h
// libraries/baking/src
//
// Created by Utkarsh Gautam on 9/29/17.
// Copyright 2017 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
//
#ifndef hifi_ModelBaker_h
#define hifi_ModelBaker_h
#include <QtCore/QFutureSynchronizer>
#include <QtCore/QDir>
#include <QtCore/QUrl>
#include <QtNetwork/QNetworkReply>
#include "Baker.h"
#include "TextureBaker.h"
#include "ModelBakingLoggingCategory.h"
#include <gpu/Texture.h>
#include <FBX.h>
using TextureBakerThreadGetter = std::function<QThread*()>;
using GetMaterialIDCallback = std::function <int(int)>;
class ModelBaker : public Baker {
Q_OBJECT
public:
ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "");
virtual ~ModelBaker();
bool compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr);
QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE);
virtual void setWasAborted(bool wasAborted) override;
QUrl getModelURL() const { return _modelURL; }
QString getBakedModelFilePath() const { return _bakedModelFilePath; }
public slots:
virtual void abort() override;
protected:
void checkIfTexturesFinished();
QHash<QByteArray, QByteArray> _textureContentMap;
QUrl _modelURL;
QString _bakedOutputDir;
QString _originalOutputDir;
QString _bakedModelFilePath;
QDir _modelTempDir;
QString _originalModelFilePath;
private slots:
void handleBakedTexture();
void handleAbortedTexture();
private:
QString createBakedTextureFileName(const QFileInfo & textureFileInfo);
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir,
const QString & bakedFilename, const QByteArray & textureContent);
QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL);
TextureBakerThreadGetter _textureThreadGetter;
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
QHash<QUrl, QString> _remappedTexturePaths;
bool _pendingErrorEmission{ false };
};
#endif // hifi_ModelBaker_h

View file

@ -0,0 +1,404 @@
//
// OBJBaker.cpp
// libraries/baking/src
//
// Created by Utkarsh Gautam on 9/29/17.
// Copyright 2017 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 <PathUtils.h>
#include <NetworkAccessManager.h>
#include "OBJBaker.h"
#include "OBJReader.h"
#include "FBXWriter.h"
const double UNIT_SCALE_FACTOR = 100.0;
const QByteArray PROPERTIES70_NODE_NAME = "Properties70";
const QByteArray P_NODE_NAME = "P";
const QByteArray C_NODE_NAME = "C";
const QByteArray FBX_HEADER_EXTENSION = "FBXHeaderExtension";
const QByteArray GLOBAL_SETTINGS_NODE_NAME = "GlobalSettings";
const QByteArray OBJECTS_NODE_NAME = "Objects";
const QByteArray GEOMETRY_NODE_NAME = "Geometry";
const QByteArray MODEL_NODE_NAME = "Model";
const QByteArray MATERIAL_NODE_NAME = "Material";
const QByteArray TEXTURE_NODE_NAME = "Texture";
const QByteArray TEXTURENAME_NODE_NAME = "TextureName";
const QByteArray RELATIVEFILENAME_NODE_NAME = "RelativeFilename";
const QByteArray CONNECTIONS_NODE_NAME = "Connections";
const QByteArray CONNECTIONS_NODE_PROPERTY = "OO";
const QByteArray CONNECTIONS_NODE_PROPERTY_1 = "OP";
const QByteArray MESH = "Mesh";
void OBJBaker::bake() {
qDebug() << "OBJBaker" << _modelURL << "bake starting";
// trigger bakeOBJ once OBJ is loaded
connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ);
// make a local copy of the OBJ
loadOBJ();
}
void OBJBaker::loadOBJ() {
if (!QDir().mkpath(_bakedOutputDir)) {
handleError("Failed to create baked OBJ output folder " + _bakedOutputDir);
return;
}
if (!QDir().mkpath(_originalOutputDir)) {
handleError("Failed to create original OBJ output folder " + _originalOutputDir);
return;
}
// check if the OBJ is local or it needs to be downloaded
if (_modelURL.isLocalFile()) {
// loading the local OBJ
QFile localOBJ { _modelURL.toLocalFile() };
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath;
if (!localOBJ.exists()) {
handleError("Could not find " + _modelURL.toString());
return;
}
// make a copy in the output folder
if (!_originalOutputDir.isEmpty()) {
qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName();
localOBJ.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
localOBJ.copy(_originalModelFilePath);
// local OBJ is loaded emit signal to trigger its baking
emit OBJLoaded();
} else {
// OBJ is remote, start download
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
// setup the request to follow re-directs and always hit the network
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_modelURL);
qCDebug(model_baking) << "Downloading" << _modelURL;
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply);
}
}
void OBJBaker::handleOBJNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded" << _modelURL;
// grab the contents of the reply and make a copy in the output folder
QFile copyOfOriginal(_originalModelFilePath);
qDebug(model_baking) << "Writing copy of original obj to" << _originalModelFilePath << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
// add an error to the error list for this obj stating that a duplicate of the original obj could not be made
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")");
return;
}
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)");
return;
}
// close that file now that we are done writing to it
copyOfOriginal.close();
if (!_originalOutputDir.isEmpty()) {
copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
// remote OBJ is loaded emit signal to trigger its baking
emit OBJLoaded();
} else {
// add an error to our list stating that the OBJ could not be downloaded
handleError("Failed to download " + _modelURL.toString());
}
}
void OBJBaker::bakeOBJ() {
// Read the OBJ file
QFile objFile(_originalModelFilePath);
if (!objFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalModelFilePath + " for reading");
return;
}
QByteArray objData = objFile.readAll();
bool combineParts = true; // set true so that OBJReader reads material info from material library
OBJReader reader;
auto geometry = reader.readOBJ(objData, QVariantHash(), combineParts, _modelURL);
// Write OBJ Data as FBX tree nodes
FBXNode rootNode;
createFBXNodeTree(rootNode, *geometry);
// Serialize the resultant FBX tree
auto encodedFBX = FBXWriter::encodeFBX(rootNode);
// Export as baked FBX
auto fileName = _modelURL.fileName();
auto baseName = fileName.left(fileName.lastIndexOf('.'));
auto bakedFilename = baseName + ".baked.fbx";
_bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename;
QFile bakedFile;
bakedFile.setFileName(_bakedModelFilePath);
if (!bakedFile.open(QIODevice::WriteOnly)) {
handleError("Error opening " + _bakedModelFilePath + " for writing");
return;
}
bakedFile.write(encodedFBX);
// Export successful
_outputFiles.push_back(_bakedModelFilePath);
qCDebug(model_baking) << "Exported" << _modelURL << "to" << _bakedModelFilePath;
checkIfTexturesFinished();
}
void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) {
// Generating FBX Header Node
FBXNode headerNode;
headerNode.name = FBX_HEADER_EXTENSION;
// Generating global settings node
// Required for Unit Scale Factor
FBXNode globalSettingsNode;
globalSettingsNode.name = GLOBAL_SETTINGS_NODE_NAME;
// Setting the tree hierarchy: GlobalSettings -> Properties70 -> P -> Properties
FBXNode properties70Node;
properties70Node.name = PROPERTIES70_NODE_NAME;
FBXNode pNode;
{
pNode.name = P_NODE_NAME;
pNode.properties.append({
"UnitScaleFactor", "double", "Number", "",
UNIT_SCALE_FACTOR
});
}
properties70Node.children = { pNode };
globalSettingsNode.children = { properties70Node };
// Generating Object node
_objectNode.name = OBJECTS_NODE_NAME;
// Generating Object node's child - Geometry node
FBXNode geometryNode;
geometryNode.name = GEOMETRY_NODE_NAME;
{
_geometryID = nextNodeID();
geometryNode.properties = {
_geometryID,
GEOMETRY_NODE_NAME,
MESH
};
}
// Compress the mesh information and store in dracoNode
bool hasDeformers = false; // No concept of deformers for an OBJ
FBXNode dracoNode;
compressMesh(geometry.meshes[0], hasDeformers, dracoNode);
geometryNode.children.append(dracoNode);
// Generating Object node's child - Model node
FBXNode modelNode;
modelNode.name = MODEL_NODE_NAME;
{
_modelID = nextNodeID();
modelNode.properties = { _modelID, MODEL_NODE_NAME, MESH };
}
_objectNode.children = { geometryNode, modelNode };
// Generating Objects node's child - Material node
auto& meshParts = geometry.meshes[0].parts;
for (auto& meshPart : meshParts) {
FBXNode materialNode;
materialNode.name = MATERIAL_NODE_NAME;
if (geometry.materials.size() == 1) {
// case when no material information is provided, OBJReader considers it as a single default material
for (auto& materialID : geometry.materials.keys()) {
setMaterialNodeProperties(materialNode, materialID, geometry);
}
} else {
setMaterialNodeProperties(materialNode, meshPart.materialID, geometry);
}
_objectNode.children.append(materialNode);
}
// Generating Texture Node
// iterate through mesh parts and process the associated textures
auto size = meshParts.size();
for (int i = 0; i < size; i++) {
QString material = meshParts[i].materialID;
FBXMaterial currentMaterial = geometry.materials[material];
if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) {
_textureID = nextNodeID();
_mapTextureMaterial.emplace_back(_textureID, i);
FBXNode textureNode;
{
textureNode.name = TEXTURE_NODE_NAME;
textureNode.properties = { _textureID };
}
// Texture node child - TextureName node
FBXNode textureNameNode;
{
textureNameNode.name = TEXTURENAME_NODE_NAME;
QByteArray propertyString = (!currentMaterial.albedoTexture.filename.isEmpty()) ? "Kd" : "Ka";
textureNameNode.properties = { propertyString };
}
// Texture node child - Relative Filename node
FBXNode relativeFilenameNode;
{
relativeFilenameNode.name = RELATIVEFILENAME_NODE_NAME;
}
QByteArray textureFileName = (!currentMaterial.albedoTexture.filename.isEmpty()) ? currentMaterial.albedoTexture.filename : currentMaterial.specularTexture.filename;
auto textureType = (!currentMaterial.albedoTexture.filename.isEmpty()) ? image::TextureUsage::Type::ALBEDO_TEXTURE : image::TextureUsage::Type::SPECULAR_TEXTURE;
// Compress the texture using ModelBaker::compressTexture() and store compressed file's name in the node
auto textureFile = compressTexture(textureFileName, textureType);
if (textureFile.isNull()) {
// Baking failed return
handleError("Failed to compress texture: " + textureFileName);
return;
}
relativeFilenameNode.properties = { textureFile };
textureNode.children = { textureNameNode, relativeFilenameNode };
_objectNode.children.append(textureNode);
}
}
// Generating Connections node
FBXNode connectionsNode;
connectionsNode.name = CONNECTIONS_NODE_NAME;
// connect Geometry to Model
FBXNode cNode;
cNode.name = C_NODE_NAME;
cNode.properties = { CONNECTIONS_NODE_PROPERTY, _geometryID, _modelID };
connectionsNode.children = { cNode };
// connect all materials to model
for (auto& materialID : _materialIDs) {
FBXNode cNode;
cNode.name = C_NODE_NAME;
cNode.properties = { CONNECTIONS_NODE_PROPERTY, materialID, _modelID };
connectionsNode.children.append(cNode);
}
// Connect textures to materials
for (const auto& texMat : _mapTextureMaterial) {
FBXNode cAmbientNode;
cAmbientNode.name = C_NODE_NAME;
cAmbientNode.properties = {
CONNECTIONS_NODE_PROPERTY_1,
texMat.first,
_materialIDs[texMat.second],
"AmbientFactor"
};
connectionsNode.children.append(cAmbientNode);
FBXNode cDiffuseNode;
cDiffuseNode.name = C_NODE_NAME;
cDiffuseNode.properties = {
CONNECTIONS_NODE_PROPERTY_1,
texMat.first,
_materialIDs[texMat.second],
"DiffuseColor"
};
connectionsNode.children.append(cDiffuseNode);
}
// Make all generated nodes children of rootNode
rootNode.children = { globalSettingsNode, _objectNode, connectionsNode };
}
// Set properties for material nodes
void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry) {
auto materialID = nextNodeID();
_materialIDs.push_back(materialID);
materialNode.properties = { materialID, material, MESH };
FBXMaterial currentMaterial = geometry.materials[material];
// Setting the hierarchy: Material -> Properties70 -> P -> Properties
FBXNode properties70Node;
properties70Node.name = PROPERTIES70_NODE_NAME;
// Set diffuseColor
FBXNode pNodeDiffuseColor;
{
pNodeDiffuseColor.name = P_NODE_NAME;
pNodeDiffuseColor.properties.append({
"DiffuseColor", "Color", "", "A",
currentMaterial.diffuseColor[0], currentMaterial.diffuseColor[1], currentMaterial.diffuseColor[2]
});
}
properties70Node.children.append(pNodeDiffuseColor);
// Set specularColor
FBXNode pNodeSpecularColor;
{
pNodeSpecularColor.name = P_NODE_NAME;
pNodeSpecularColor.properties.append({
"SpecularColor", "Color", "", "A",
currentMaterial.specularColor[0], currentMaterial.specularColor[1], currentMaterial.specularColor[2]
});
}
properties70Node.children.append(pNodeSpecularColor);
// Set Shininess
FBXNode pNodeShininess;
{
pNodeShininess.name = P_NODE_NAME;
pNodeShininess.properties.append({
"Shininess", "Number", "", "A",
currentMaterial.shininess
});
}
properties70Node.children.append(pNodeShininess);
// Set Opacity
FBXNode pNodeOpacity;
{
pNodeOpacity.name = P_NODE_NAME;
pNodeOpacity.properties.append({
"Opacity", "Number", "", "A",
currentMaterial.opacity
});
}
properties70Node.children.append(pNodeOpacity);
materialNode.children.append(properties70Node);
}

View file

@ -0,0 +1,54 @@
//
// OBJBaker.h
// libraries/baking/src
//
// Created by Utkarsh Gautam on 9/29/17.
// Copyright 2017 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
//
#ifndef hifi_OBJBaker_h
#define hifi_OBJBaker_h
#include "Baker.h"
#include "TextureBaker.h"
#include "ModelBaker.h"
#include "ModelBakingLoggingCategory.h"
using TextureBakerThreadGetter = std::function<QThread*()>;
using NodeID = qlonglong;
class OBJBaker : public ModelBaker {
Q_OBJECT
public:
using ModelBaker::ModelBaker;
public slots:
virtual void bake() override;
signals:
void OBJLoaded();
private slots:
void bakeOBJ();
void handleOBJNetworkReply();
private:
void loadOBJ();
void createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry);
void setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry);
NodeID nextNodeID() { return _nodeID++; }
NodeID _nodeID { 0 };
NodeID _geometryID;
NodeID _modelID;
std::vector<NodeID> _materialIDs;
NodeID _textureID;
std::vector<std::pair<NodeID, int>> _mapTextureMaterial;
FBXNode _objectNode;
};
#endif // hifi_OBJBaker_h

View file

@ -161,23 +161,19 @@ void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) {
case QMetaType::QString:
{
auto bytes = prop.toString().toUtf8();
out << 'S';
out << bytes.length();
out << bytes;
out.device()->write("S", 1);
out << (int32_t)bytes.size();
out.writeRawData(bytes, bytes.size());
break;
}
case QMetaType::QByteArray:
{
auto bytes = prop.toByteArray();
out.device()->write("S", 1);
out << (int32_t)bytes.size();
out.writeRawData(bytes, bytes.size());
break;
}
{
auto bytes = prop.toByteArray();
out.device()->write("S", 1);
out << (int32_t)bytes.size();
out.writeRawData(bytes, bytes.size());
break;
}
default:
{
if (prop.canConvert<QVector<float>>()) {

View file

@ -643,13 +643,13 @@ done:
}
FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url) {
FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url) {
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr);
QBuffer buffer { &model };
buffer.open(QIODevice::ReadOnly);
FBXGeometry* geometryPtr = new FBXGeometry();
FBXGeometry& geometry = *geometryPtr;
auto geometryPtr { std::make_shared<FBXGeometry>() };
FBXGeometry& geometry { *geometryPtr };
OBJTokenizer tokenizer { &buffer };
float scaleGuess = 1.0f;

View file

@ -85,7 +85,7 @@ public:
QString currentMaterialName;
QHash<QString, OBJMaterial> materials;
FBXGeometry* readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl());
FBXGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl());
private:
QUrl _url;

View file

@ -190,11 +190,11 @@ void GeometryReader::run() {
throw QString("empty geometry, possibly due to an unsupported FBX version");
}
} else if (_url.path().toLower().endsWith(".obj")) {
fbxGeometry.reset(OBJReader().readOBJ(_data, _mapping, _combineParts, _url));
fbxGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url);
} else if (_url.path().toLower().endsWith(".obj.gz")) {
QByteArray uncompressedData;
if (gunzip(_data, uncompressedData)) {
fbxGeometry.reset(OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url));
if (gunzip(_data, uncompressedData)){
fbxGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url);
} else {
throw QString("failed to decompress .obj.gz");
}

View file

@ -26,12 +26,14 @@
#include "NetworkAccessManager.h"
#include "NetworkLogging.h"
ResourceManager::ResourceManager() {
ResourceManager::ResourceManager(bool atpSupportEnabled) : _atpSupportEnabled(atpSupportEnabled) {
_thread.setObjectName("Resource Manager Thread");
auto assetClient = DependencyManager::set<AssetClient>();
assetClient->moveToThread(&_thread);
QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::initCaching);
if (_atpSupportEnabled) {
auto assetClient = DependencyManager::set<AssetClient>();
assetClient->moveToThread(&_thread);
QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::initCaching);
}
_thread.start();
}
@ -111,6 +113,10 @@ ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const Q
} else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) {
request = new HTTPResourceRequest(normalizedURL);
} else if (scheme == URL_SCHEME_ATP) {
if (!_atpSupportEnabled) {
qCDebug(networking) << "ATP support not enabled, unable to create request for URL: " << url.url();
return nullptr;
}
request = new AssetResourceRequest(normalizedURL);
} else {
qCDebug(networking) << "Unknown scheme (" << scheme << ") for URL: " << url.url();
@ -146,7 +152,7 @@ bool ResourceManager::resourceExists(const QUrl& url) {
reply->deleteLater();
return reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200;
} else if (scheme == URL_SCHEME_ATP) {
} else if (scheme == URL_SCHEME_ATP && _atpSupportEnabled) {
auto request = new AssetResourceRequest(url);
ByteRange range;
range.fromInclusive = 1;

View file

@ -34,7 +34,7 @@ class ResourceManager: public QObject, public Dependency {
SINGLETON_DEPENDENCY
public:
ResourceManager();
ResourceManager(bool atpSupportEnabled = true);
void setUrlPrefixOverride(const QString& prefix, const QString& replacement);
QString normalizeURL(const QString& urlString);
@ -57,6 +57,7 @@ private:
using PrefixMap = std::map<QString, QString>;
bool _atpSupportEnabled;
PrefixMap _prefixMap;
QMutex _prefixMapLock;

View file

@ -9,7 +9,8 @@
//
/* global Tablet, Script, HMD, UserActivityLogger, Entities, Account, Wallet, ContextOverlay, Settings, Camera, Vec3,
Quat, MyAvatar, Clipboard, Menu, Grid, Uuid, GlobalServices, openLoginWindow */
Quat, MyAvatar, Clipboard, Menu, Grid, Uuid, GlobalServices, openLoginWindow, Overlays, SoundCache,
DesktopPreviewProvider */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
var selectionDisplay = null; // for gridTool.js to ignore
@ -116,6 +117,24 @@ var selectionDisplay = null; // for gridTool.js to ignore
var onWalletScreen = false;
var onCommerceScreen = false;
var tabletShouldBeVisibleInSecondaryCamera = false;
function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) {
if (visibleInSecondaryCam) {
// if we're potentially showing the tablet, only do so if it was visible before
if (!tabletShouldBeVisibleInSecondaryCamera) {
return;
}
} else {
// if we're hiding the tablet, check to see if it was visible in the first place
tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera");
}
Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
Overlays.editOverlay(HMD.homeButtonHighlightIDtabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera : visibleInSecondaryCam });
}
function onScreenChanged(type, url) {
onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1;
@ -127,6 +146,7 @@ var selectionDisplay = null; // for gridTool.js to ignore
if (isHmdPreviewDisabledBySecurity) {
DesktopPreviewProvider.setPreviewDisabledReason("USER");
Menu.setIsOptionChecked("Disable Preview", false);
setTabletVisibleInSecondaryCamera(true);
isHmdPreviewDisabledBySecurity = false;
}
}
@ -245,7 +265,7 @@ var selectionDisplay = null; // for gridTool.js to ignore
var wearableDimensions = null;
if (itemType === "contentSet") {
console.log("Item is a content set; codepath shouldn't go here.")
console.log("Item is a content set; codepath shouldn't go here.");
return;
}
@ -575,6 +595,7 @@ var selectionDisplay = null; // for gridTool.js to ignore
if (!isHmdPreviewDisabled) {
DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN");
Menu.setIsOptionChecked("Disable Preview", true);
setTabletVisibleInSecondaryCamera(false);
isHmdPreviewDisabledBySecurity = true;
}
break;
@ -582,6 +603,7 @@ var selectionDisplay = null; // for gridTool.js to ignore
if (isHmdPreviewDisabledBySecurity) {
DesktopPreviewProvider.setPreviewDisabledReason("USER");
Menu.setIsOptionChecked("Disable Preview", false);
setTabletVisibleInSecondaryCamera(true);
isHmdPreviewDisabledBySecurity = false;
}
break;

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "DomainBaker.h"
#include <QtConcurrent>
#include <QtCore/QEventLoop>
#include <QtCore/QFile>
@ -17,10 +19,9 @@
#include <QtCore/QJsonObject>
#include "Gzip.h"
#include "Oven.h"
#include "DomainBaker.h"
#include "FBXBaker.h"
#include "OBJBaker.h"
DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName,
const QString& baseOutputPath, const QUrl& destinationPath,
@ -167,15 +168,18 @@ void DomainBaker::enumerateEntities() {
// check if the file pointed to by this URL is a bakeable model, by comparing extensions
auto modelFileName = modelURL.fileName();
static const QString BAKEABLE_MODEL_EXTENSION { ".fbx" };
static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" };
static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" };
static const QString BAKED_MODEL_EXTENSION = ".baked.fbx";
bool isBakedFBX = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive);
bool isUnbakedFBX = modelFileName.endsWith(BAKEABLE_MODEL_EXTENSION, Qt::CaseInsensitive) && !isBakedFBX;
bool isBakedModel = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive);
bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive);
bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive);
bool isBakeable = isBakeableFBX || isBakeableOBJ;
if (isUnbakedFBX || (_shouldRebakeOriginals && isBakedFBX)) {
if (isBakeable || (_shouldRebakeOriginals && isBakedModel)) {
if (isBakedFBX) {
if (isBakedModel) {
// grab a URL to the original, that we assume is stored a directory up, in the "original" folder
// with just the fbx extension
qDebug() << "Re-baking original for" << modelURL;
@ -190,7 +194,7 @@ void DomainBaker::enumerateEntities() {
modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
}
// setup an FBXBaker for this URL, as long as we don't already have one
// setup a ModelBaker for this URL, as long as we don't already have one
if (!_modelBakers.contains(modelURL)) {
auto filename = modelURL.fileName();
auto baseName = filename.left(filename.lastIndexOf('.'));
@ -199,12 +203,23 @@ void DomainBaker::enumerateEntities() {
while (QDir(_contentOutputPath + subDirName).exists()) {
subDirName = "/" + baseName + "-" + QString::number(i++);
}
QSharedPointer<FBXBaker> baker {
new FBXBaker(modelURL, []() -> QThread* {
return Oven::instance().getNextWorkerThread();
}, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"),
&FBXBaker::deleteLater
};
QSharedPointer<ModelBaker> baker;
if (isBakeableFBX) {
baker = {
new FBXBaker(modelURL, []() -> QThread* {
return Oven::instance().getNextWorkerThread();
}, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"),
&FBXBaker::deleteLater
};
} else {
baker = {
new OBJBaker(modelURL, []() -> QThread* {
return Oven::instance().getNextWorkerThread();
}, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"),
&OBJBaker::deleteLater
};
}
// make sure our handler is called when the baker is done
connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker);
@ -299,16 +314,16 @@ void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) {
}
void DomainBaker::handleFinishedModelBaker() {
auto baker = qobject_cast<FBXBaker*>(sender());
auto baker = qobject_cast<ModelBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this FBXBaker is done and everything went according to plan
qDebug() << "Re-writing entity references to" << baker->getFBXUrl();
qDebug() << "Re-writing entity references to" << baker->getModelURL();
// enumerate the QJsonRef values for the URL of this FBX from our multi hash of
// entity objects needing a URL re-write
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getFBXUrl())) {
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getModelURL())) {
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
auto entity = entityValue.toObject();
@ -317,7 +332,7 @@ void DomainBaker::handleFinishedModelBaker() {
QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() };
// setup a new URL using the prefix we were passed
auto relativeFBXFilePath = baker->getBakedFBXFilePath().remove(_contentOutputPath);
auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath);
if (relativeFBXFilePath.startsWith("/")) {
relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1);
}
@ -370,10 +385,10 @@ void DomainBaker::handleFinishedModelBaker() {
}
// remove the baked URL from the multi hash of entities needing a re-write
_entitiesNeedingRewrite.remove(baker->getFBXUrl());
_entitiesNeedingRewrite.remove(baker->getModelURL());
// drop our shared pointer to this baker so that it gets cleaned up
_modelBakers.remove(baker->getFBXUrl());
_modelBakers.remove(baker->getModelURL());
// emit progress to tell listeners how many models we have baked
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);

View file

@ -61,7 +61,7 @@ private:
QJsonArray _entities;
QHash<QUrl, QSharedPointer<FBXBaker>> _modelBakers;
QHash<QUrl, QSharedPointer<ModelBaker>> _modelBakers;
QHash<QUrl, QSharedPointer<TextureBaker>> _skyboxBakers;
QMultiHash<QUrl, QJsonValueRef> _entitiesNeedingRewrite;

View file

@ -9,12 +9,16 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "Oven.h"
#include <QtCore/QDebug>
#include <QtCore/QThread>
#include <image/Image.h>
#include "Oven.h"
#include <DependencyManager.h>
#include <StatTracker.h>
#include <ResourceManager.h>
Oven* Oven::_staticInstance { nullptr };
@ -29,6 +33,10 @@ Oven::Oven() {
// setup our worker threads
setupWorkerThreads(QThread::idealThreadCount());
// Initialize dependencies for OBJ Baker
DependencyManager::set<StatTracker>();
DependencyManager::set<ResourceManager>(false);
}
Oven::~Oven() {

View file

@ -14,6 +14,9 @@
#include <atomic>
#include <memory>
#include <vector>
class QThread;
class Oven {
@ -31,7 +34,7 @@ private:
std::vector<std::unique_ptr<QThread>> _workerThreads;
std::atomic<uint> _nextWorkerThreadIndex;
std::atomic<uint32_t> _nextWorkerThreadIndex;
int _numWorkerThreads;
static Oven* _staticInstance;

View file

@ -41,5 +41,5 @@ void BakeWidget::cancelButtonClicked() {
auto stackedWidget = qobject_cast<QStackedWidget*>(parentWidget());
stackedWidget->removeWidget(this);
this->deleteLater();
deleteLater();
}

View file

@ -21,18 +21,21 @@
#include <QtCore/QDebug>
#include <QtCore/QThread>
#include "../Oven.h"
#include "../OvenGUIApplication.h"
#include "OvenMainWindow.h"
#include "FBXBaker.h"
#include "OBJBaker.h"
#include "ModelBakeWidget.h"
static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory";
static const auto MODEL_START_DIR_SETTING_KEY = "model_search_directory";
ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) :
BakeWidget(parent, flags),
_exportDirectory(EXPORT_DIR_SETTING_KEY),
_modelStartDirectory(MODEL_START_DIR_SETTING_KEY)
{
_modelStartDirectory(MODEL_START_DIR_SETTING_KEY) {
setupUI();
}
@ -113,7 +116,7 @@ void ModelBakeWidget::chooseFileButtonClicked() {
startDir = QDir::homePath();
}
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx)");
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj)");
if (!selectedFiles.isEmpty()) {
// set the contents of the model file text box to be the path to the selected file
@ -189,7 +192,7 @@ void ModelBakeWidget::bakeButtonClicked() {
subFolderName = modelName + "-" + QString::number(++iteration) + "/";
}
outputDirectory.mkdir(subFolderName);
outputDirectory.mkpath(subFolderName);
if (!outputDirectory.exists()) {
QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory.");
@ -200,16 +203,25 @@ void ModelBakeWidget::bakeButtonClicked() {
QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked");
QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original");
bakedOutputDirectory.mkdir(".");
originalOutputDirectory.mkdir(".");
// everything seems to be in place, kick off a bake for this model now
auto baker = std::unique_ptr<FBXBaker> {
new FBXBaker(modelToBakeURL, []() -> QThread* {
return Oven::instance().getNextWorkerThread();
}, bakedOutputDirectory.absolutePath(), originalOutputDirectory.absolutePath())
std::unique_ptr<Baker> baker;
auto getWorkerThreadCallback = []() -> QThread* {
return Oven::instance().getNextWorkerThread();
};
// everything seems to be in place, kick off a bake for this model now
if (modelToBakeURL.fileName().endsWith(".fbx")) {
baker.reset(new FBXBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(),
originalOutputDirectory.absolutePath()));
} else if (modelToBakeURL.fileName().endsWith(".obj")) {
baker.reset(new OBJBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(),
originalOutputDirectory.absolutePath()));
} else {
qWarning() << "Unknown model type: " << modelToBakeURL.fileName();
continue;
}
// move the baker to the FBX baker thread
baker->moveToThread(Oven::instance().getNextWorkerThread());
@ -218,7 +230,7 @@ void ModelBakeWidget::bakeButtonClicked() {
QMetaObject::invokeMethod(baker.get(), "bake");
// make sure we hear about the results of this baker when it is done
connect(baker.get(), &FBXBaker::finished, this, &ModelBakeWidget::handleFinishedBaker);
connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker);
// add a pending row to the results window to show that this bake is in process
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
@ -231,27 +243,31 @@ void ModelBakeWidget::bakeButtonClicked() {
}
void ModelBakeWidget::handleFinishedBaker() {
if (auto baker = qobject_cast<FBXBaker*>(sender())) {
// add the results of this bake to the results window
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
return value.first.get() == baker;
});
Baker* baker = dynamic_cast<Baker*>(sender());
if (!baker) {
qWarning() << "Received signal from unexpected sender";
return;
}
for (auto& file : baker->getOutputFiles()) {
qDebug() << "Baked file: " << file;
// add the results of this bake to the results window
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
return value.first.get() == baker;
});
for (auto& file : baker->getOutputFiles()) {
qDebug() << "Baked file: " << file;
}
if (it != _bakers.end()) {
auto resultRow = it->second;
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
if (baker->hasErrors()) {
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
} else {
resultsWindow->changeStatusForRow(resultRow, "Success");
}
if (it != _bakers.end()) {
auto resultRow = it->second;
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
if (baker->hasErrors()) {
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
} else {
resultsWindow->changeStatusForRow(resultRow, "Success");
}
_bakers.erase(it);
}
_bakers.erase(it);
}
}

View file

@ -16,8 +16,6 @@
#include <SettingHandle.h>
#include <FBXBaker.h>
#include "BakeWidget.h"
class QLineEdit;

View file

@ -46,7 +46,7 @@ ResultsWindow* OvenMainWindow::showResultsWindow(bool shouldRaise) {
_resultsWindow->show();
// place the results window initially below our window
_resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom());
_resultsWindow->move(_resultsWindow->x(), frameGeometry().bottom());
}
// show the results window and make sure it is in front

View file

@ -41,18 +41,17 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) {
}
try {
QByteArray fbxContents = fbx.readAll();
FBXGeometry* geom;
FBXGeometry::Pointer geom;
if (filename.toLower().endsWith(".obj")) {
bool combineParts = false;
geom = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts);
} else if (filename.toLower().endsWith(".fbx")) {
geom = readFBX(fbxContents, QVariantHash(), filename);
geom.reset(readFBX(fbxContents, QVariantHash(), filename));
} else {
qWarning() << "file has unknown extension" << filename;
return false;
}
result = *geom;
delete geom;
reSortFBXGeometryMeshes(result);
} catch (const QString& error) {