From 197127fbde4d454707d45f9505e3d5726123a4ee Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Thu, 17 Apr 2014 15:42:23 -0700 Subject: [PATCH] Added ability to read FBX textures embedded in FBX files. --- interface/src/ModelUploader.cpp | 8 +-- interface/src/renderer/GeometryCache.cpp | 8 +-- interface/src/renderer/TextureCache.cpp | 66 ++++++++++++++++++------ interface/src/renderer/TextureCache.h | 13 +++-- libraries/fbx/src/FBXReader.cpp | 52 +++++++++++++------ libraries/fbx/src/FBXReader.h | 12 ++++- 6 files changed, 112 insertions(+), 47 deletions(-) diff --git a/interface/src/ModelUploader.cpp b/interface/src/ModelUploader.cpp index 5b2688169f..7f4ed0d36e 100644 --- a/interface/src/ModelUploader.cpp +++ b/interface/src/ModelUploader.cpp @@ -306,14 +306,14 @@ bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) { foreach (FBXMesh mesh, geometry.meshes) { foreach (FBXMeshPart part, mesh.parts) { - if (!part.diffuseFilename.isEmpty()) { - if (!addPart(texdir + "/" + part.diffuseFilename, + if (!part.diffuseTexture.filename.isEmpty()) { + if (!addPart(texdir + "/" + part.diffuseTexture.filename, QString("texture%1").arg(++_texturesCount))) { return false; } } - if (!part.normalFilename.isEmpty()) { - if (!addPart(texdir + "/" + part.normalFilename, + if (!part.normalTexture.filename.isEmpty()) { + if (!addPart(texdir + "/" + part.normalTexture.filename, QString("texture%1").arg(++_texturesCount))) { return false; } diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp index 7b4eef1ac1..3a410ac5e2 100644 --- a/interface/src/renderer/GeometryCache.cpp +++ b/interface/src/renderer/GeometryCache.cpp @@ -543,14 +543,14 @@ void NetworkGeometry::setGeometry(const FBXGeometry& geometry) { int totalIndices = 0; foreach (const FBXMeshPart& part, mesh.parts) { NetworkMeshPart networkPart; - if (!part.diffuseFilename.isEmpty()) { + if (!part.diffuseTexture.filename.isEmpty()) { networkPart.diffuseTexture = Application::getInstance()->getTextureCache()->getTexture( - _textureBase.resolved(QUrl(part.diffuseFilename)), false, mesh.isEye); + _textureBase.resolved(QUrl(part.diffuseTexture.filename)), false, mesh.isEye, part.diffuseTexture.content); networkPart.diffuseTexture->setLoadPriorities(_loadPriorities); } - if (!part.normalFilename.isEmpty()) { + if (!part.normalTexture.filename.isEmpty()) { networkPart.normalTexture = Application::getInstance()->getTextureCache()->getTexture( - _textureBase.resolved(QUrl(part.normalFilename)), true); + _textureBase.resolved(QUrl(part.normalTexture.filename)), true, false, part.normalTexture.content); networkPart.normalTexture->setLoadPriorities(_loadPriorities); } networkMesh.parts.append(networkPart); diff --git a/interface/src/renderer/TextureCache.cpp b/interface/src/renderer/TextureCache.cpp index 3f523cf4bb..f31e4f9060 100644 --- a/interface/src/renderer/TextureCache.cpp +++ b/interface/src/renderer/TextureCache.cpp @@ -105,13 +105,22 @@ GLuint TextureCache::getBlueTextureID() { return _blueTextureID; } -QSharedPointer TextureCache::getTexture(const QUrl& url, bool normalMap, bool dilatable) { +/// Extra data for creating textures. +class TextureExtra { +public: + bool normalMap; + const QByteArray& content; +}; + +QSharedPointer TextureCache::getTexture(const QUrl& url, bool normalMap, + bool dilatable, const QByteArray& content) { if (!dilatable) { - return ResourceCache::getResource(url, QUrl(), false, &normalMap).staticCast(); + TextureExtra extra = { normalMap, content }; + return ResourceCache::getResource(url, QUrl(), false, &extra).staticCast(); } QSharedPointer texture = _dilatableNetworkTextures.value(url); if (texture.isNull()) { - texture = QSharedPointer(new DilatableNetworkTexture(url), &Resource::allReferencesCleared); + texture = QSharedPointer(new DilatableNetworkTexture(url, content), &Resource::allReferencesCleared); texture->setSelf(texture); texture->setCache(this); _dilatableNetworkTextures.insert(url, texture); @@ -215,7 +224,9 @@ bool TextureCache::eventFilter(QObject* watched, QEvent* event) { QSharedPointer TextureCache::createResource(const QUrl& url, const QSharedPointer& fallback, bool delayLoad, const void* extra) { - return QSharedPointer(new NetworkTexture(url, *(const bool*)extra), &Resource::allReferencesCleared); + const TextureExtra* textureExtra = static_cast(extra); + return QSharedPointer(new NetworkTexture(url, textureExtra->normalMap, textureExtra->content), + &Resource::allReferencesCleared); } QOpenGLFramebufferObject* TextureCache::createFramebufferObject() { @@ -238,8 +249,8 @@ Texture::~Texture() { glDeleteTextures(1, &_id); } -NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) : - Resource(url), +NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap, const QByteArray& content) : + Resource(url, !content.isEmpty()), _translucent(false) { if (!url.isValid()) { @@ -250,12 +261,19 @@ NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) : glBindTexture(GL_TEXTURE_2D, getID()); loadSingleColorTexture(normalMap ? OPAQUE_BLUE : OPAQUE_WHITE); glBindTexture(GL_TEXTURE_2D, 0); + + // if we have content, load it after we have our self pointer + if (!content.isEmpty()) { + _startedLoading = true; + QMetaObject::invokeMethod(this, "loadContent", Qt::QueuedConnection, Q_ARG(const QByteArray&, content)); + } } class ImageReader : public QRunnable { public: - ImageReader(const QWeakPointer& texture, QNetworkReply* reply); + ImageReader(const QWeakPointer& texture, QNetworkReply* reply, const QUrl& url = QUrl(), + const QByteArray& content = QByteArray()); virtual void run(); @@ -263,27 +281,37 @@ private: QWeakPointer _texture; QNetworkReply* _reply; + QUrl _url; + QByteArray _content; }; -ImageReader::ImageReader(const QWeakPointer& texture, QNetworkReply* reply) : +ImageReader::ImageReader(const QWeakPointer& texture, QNetworkReply* reply, + const QUrl& url, const QByteArray& content) : _texture(texture), - _reply(reply) { + _reply(reply), + _url(url), + _content(content) { } void ImageReader::run() { QSharedPointer texture = _texture.toStrongRef(); if (texture.isNull()) { - _reply->deleteLater(); + if (_reply) { + _reply->deleteLater(); + } return; } - QUrl url = _reply->url(); - QImage image = QImage::fromData(_reply->readAll()); - _reply->deleteLater(); + if (_reply) { + _url = _reply->url(); + _content = _reply->readAll(); + _reply->deleteLater(); + } + QImage image = QImage::fromData(_content); // enforce a fixed maximum const int MAXIMUM_SIZE = 1024; if (image.width() > MAXIMUM_SIZE || image.height() > MAXIMUM_SIZE) { - qDebug() << "Image greater than maximum size:" << url << image.width() << image.height(); + qDebug() << "Image greater than maximum size:" << _url << image.width() << image.height(); image = image.scaled(MAXIMUM_SIZE, MAXIMUM_SIZE, Qt::KeepAspectRatio); } @@ -315,7 +343,7 @@ void ImageReader::run() { } int imageArea = image.width() * image.height(); if (opaquePixels == imageArea) { - qDebug() << "Image with alpha channel is completely opaque:" << url; + qDebug() << "Image with alpha channel is completely opaque:" << _url; image = image.convertToFormat(QImage::Format_RGB888); } QMetaObject::invokeMethod(texture.data(), "setImage", Q_ARG(const QImage&, image), @@ -327,6 +355,10 @@ void NetworkTexture::downloadFinished(QNetworkReply* reply) { QThreadPool::globalInstance()->start(new ImageReader(_self, reply)); } +void NetworkTexture::loadContent(const QByteArray& content) { + QThreadPool::globalInstance()->start(new ImageReader(_self, NULL, _url, content)); +} + void NetworkTexture::setImage(const QImage& image, bool translucent) { _translucent = translucent; @@ -348,8 +380,8 @@ void NetworkTexture::imageLoaded(const QImage& image) { // nothing by default } -DilatableNetworkTexture::DilatableNetworkTexture(const QUrl& url) : - NetworkTexture(url, false), +DilatableNetworkTexture::DilatableNetworkTexture(const QUrl& url, const QByteArray& content) : + NetworkTexture(url, false, content), _innerRadius(0), _outerRadius(0) { diff --git a/interface/src/renderer/TextureCache.h b/interface/src/renderer/TextureCache.h index e66044d843..f4444b6dfc 100644 --- a/interface/src/renderer/TextureCache.h +++ b/interface/src/renderer/TextureCache.h @@ -44,7 +44,8 @@ public: GLuint getBlueTextureID(); /// Loads a texture from the specified URL. - QSharedPointer getTexture(const QUrl& url, bool normalMap = false, bool dilatable = false); + QSharedPointer getTexture(const QUrl& url, bool normalMap = false, bool dilatable = false, + const QByteArray& content = QByteArray()); /// Returns a pointer to the primary framebuffer object. This render target includes a depth component, and is /// used for scene rendering. @@ -115,7 +116,7 @@ class NetworkTexture : public Resource, public Texture { public: - NetworkTexture(const QUrl& url, bool normalMap); + NetworkTexture(const QUrl& url, bool normalMap, const QByteArray& content); /// Checks whether it "looks like" this texture is translucent /// (majority of pixels neither fully opaque or fully transparent). @@ -124,10 +125,12 @@ public: protected: virtual void downloadFinished(QNetworkReply* reply); - virtual void imageLoaded(const QImage& image); - + + Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(const QImage& image, bool translucent); + virtual void imageLoaded(const QImage& image); + private: bool _translucent; @@ -139,7 +142,7 @@ class DilatableNetworkTexture : public NetworkTexture { public: - DilatableNetworkTexture(const QUrl& url); + DilatableNetworkTexture(const QUrl& url, const QByteArray& content); /// Returns a pointer to a texture with the requested amount of dilation. QSharedPointer getDilatedTexture(float dilation); diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index eae8eb3920..dc4e7f617e 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -935,6 +935,14 @@ public: QVector values; }; +FBXTexture getTexture(const QString& textureID, const QHash& textureFilenames, + const QHash& textureContent) { + FBXTexture texture; + texture.filename = textureFilenames.value(textureID); + texture.content = textureContent.value(texture.filename); + return texture; +} + FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) { QHash meshes; QVector blendshapes; @@ -944,6 +952,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QHash clusters; QHash animationCurves; QHash textureFilenames; + QHash textureContent; QHash materials; QHash diffuseTextures; QHash bumpTextures; @@ -952,8 +961,6 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) QHash yComponents; QHash zComponents; - printNode(node, 0); - QVariantHash joints = mapping.value("joint").toHash(); QString jointEyeLeftName = processID(getString(joints.value("jointEyeLeft", "jointEyeLeft"))); QString jointEyeRightName = processID(getString(joints.value("jointEyeRight", "jointEyeRight"))); @@ -1182,6 +1189,21 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) textureFilenames.insert(getID(object.properties), filename); } } + } else if (object.name == "Video") { + QByteArray filename; + QByteArray content; + foreach (const FBXNode& subobject, object.children) { + if (subobject.name == "RelativeFilename") { + filename = subobject.properties.at(0).toByteArray(); + filename = filename.mid(qMax(filename.lastIndexOf('\\'), filename.lastIndexOf('/')) + 1); + + } else if (subobject.name == "Content" && !subobject.properties.isEmpty()) { + content = subobject.properties.at(0).toByteArray(); + } + } + if (!content.isEmpty()) { + textureContent.insert(filename, content); + } } else if (object.name == "Material") { Material material = { glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(1.0f, 1.0f, 1.0f), 96.0f }; foreach (const FBXNode& subobject, object.children) { @@ -1263,7 +1285,7 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) if (type.contains("diffuse")) { diffuseTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); - } else if (type.contains("bump")) { + } else if (type.contains("bump") || type.contains("normal")) { bumpTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type == "lcl rotation") { @@ -1463,23 +1485,23 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) if (materials.contains(childID)) { Material material = materials.value(childID); - QByteArray diffuseFilename; + FBXTexture diffuseTexture; QString diffuseTextureID = diffuseTextures.value(childID); if (!diffuseTextureID.isNull()) { - diffuseFilename = textureFilenames.value(diffuseTextureID); - + diffuseTexture = getTexture(diffuseTextureID, textureFilenames, textureContent); + // FBX files generated by 3DSMax have an intermediate texture parent, apparently foreach (const QString& childTextureID, childMap.values(diffuseTextureID)) { if (textureFilenames.contains(childTextureID)) { - diffuseFilename = textureFilenames.value(childTextureID); + diffuseTexture = getTexture(diffuseTextureID, textureFilenames, textureContent); } } } - QByteArray normalFilename; + FBXTexture normalTexture; QString bumpTextureID = bumpTextures.value(childID); if (!bumpTextureID.isNull()) { - normalFilename = textureFilenames.value(bumpTextureID); + normalTexture = getTexture(bumpTextureID, textureFilenames, textureContent); generateTangents = true; } @@ -1489,21 +1511,21 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) part.diffuseColor = material.diffuse; part.specularColor = material.specular; part.shininess = material.shininess; - if (!diffuseFilename.isNull()) { - part.diffuseFilename = diffuseFilename; + if (!diffuseTexture.filename.isNull()) { + part.diffuseTexture = diffuseTexture; } - if (!normalFilename.isNull()) { - part.normalFilename = normalFilename; + if (!normalTexture.filename.isNull()) { + part.normalTexture = normalTexture; } } } materialIndex++; } else if (textureFilenames.contains(childID)) { - QByteArray filename = textureFilenames.value(childID); + FBXTexture texture = getTexture(childID, textureFilenames, textureContent); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { if (extracted.partMaterialTextures.at(j).second == textureIndex) { - extracted.mesh.parts[j].diffuseFilename = filename; + extracted.mesh.parts[j].diffuseTexture = texture; } } textureIndex++; diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 2f840e868e..9445daa7df 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -103,6 +103,14 @@ public: glm::mat4 inverseBindMatrix; }; +/// A texture map in an FBX document. +class FBXTexture { +public: + + QByteArray filename; + QByteArray content; +}; + /// A single part of a mesh (with the same material). class FBXMeshPart { public: @@ -114,8 +122,8 @@ public: glm::vec3 specularColor; float shininess; - QByteArray diffuseFilename; - QByteArray normalFilename; + FBXTexture diffuseTexture; + FBXTexture normalTexture; }; /// A single mesh (with optional blendshapes) extracted from an FBX document.