From 7f05e4453b6821bf6526255691522c83d431d1a2 Mon Sep 17 00:00:00 2001
From: Zach Pomerantz <zach@highfidelity.io>
Date: Wed, 23 Mar 2016 14:01:22 -0700
Subject: [PATCH] Fetch model geometries through cache

---
 .../src/model-networking/ModelCache.cpp       | 821 ++++++++----------
 .../src/model-networking/ModelCache.h         | 266 +++---
 2 files changed, 493 insertions(+), 594 deletions(-)

diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp
index b0b769d5e9..bb7efb8f75 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.cpp
+++ b/libraries/model-networking/src/model-networking/ModelCache.cpp
@@ -1,53 +1,101 @@
 //
 //  ModelCache.cpp
-//  interface/src/renderer
+//  libraries/model-networking
 //
-//  Created by Andrzej Kapolka on 6/21/13.
-//  Copyright 2013 High Fidelity, Inc.
+//  Created by Zach Pomerantz on 3/15/16.
+//  Copyright 2016 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 "ModelCache.h"
+#include <FSTReader.h>
+#include "FBXReader.h"
+#include "OBJReader.h"
 
-#include <cmath>
+#include <gpu/Batch.h>
+#include <gpu/Stream.h>
 
-#include <QNetworkReply>
 #include <QThreadPool>
 
-#include <FSTReader.h>
-#include <NumericalConstants.h>
-
-#include "TextureCache.h"
 #include "ModelNetworkingLogging.h"
 
-#include "model/TextureMap.h"
+class GeometryReader;
 
-//#define WANT_DEBUG
+class GeometryExtra {
+public:
+    const QVariantHash& mapping;
+    const QUrl& textureBaseUrl;
+};
 
-ModelCache::ModelCache()
-{
-    const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE;
-    setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE);
+class GeometryMappingResource : public GeometryResource {
+    Q_OBJECT
+public:
+    GeometryMappingResource(const QUrl& url) : GeometryResource(url) {};
+
+    virtual void downloadFinished(const QByteArray& data) override;
+
+private slots:
+    void onGeometryMappingLoaded(bool success);
+
+private:
+    GeometryResource::Pointer _geometryResource;
+};
+
+void GeometryMappingResource::downloadFinished(const QByteArray& data) {
+    auto mapping = FSTReader::readMapping(data);
+
+    QString filename = mapping.value("filename").toString();
+    if (filename.isNull()) {
+        qCDebug(modelnetworking) << "Mapping file" << _url << "has no \"filename\" field";
+        finishedLoading(false);
+    } else {
+        QUrl url = _url.resolved(filename);
+        QUrl textureBaseUrl;
+
+        QString texdir = mapping.value("texdir").toString();
+        if (!texdir.isNull()) {
+            if (!texdir.endsWith('/')) {
+                texdir += '/';
+            }
+            textureBaseUrl = _url.resolved(texdir);
+        }
+
+        auto modelCache = DependencyManager::get<ModelCache>();
+        GeometryExtra extra{ mapping, textureBaseUrl };
+
+        // Get the raw GeometryResource, not the wrapped NetworkGeometry
+        _geometryResource = modelCache->getResource(url, QUrl(), true, &extra).staticCast<GeometryResource>();
+        connect(_geometryResource.data(), &Resource::finished, this, &GeometryMappingResource::onGeometryMappingLoaded);
+    }
 }
 
-ModelCache::~ModelCache() {
+void GeometryMappingResource::onGeometryMappingLoaded(bool success) {
+    if (success) {
+        _geometry = _geometryResource->_geometry;
+        _shapes = _geometryResource->_shapes;
+        _meshes = _geometryResource->_meshes;
+        _materials = _geometryResource->_materials;
+    }
+    finishedLoading(success);
 }
 
-QSharedPointer<Resource> ModelCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-                                                        bool delayLoad, const void* extra) {
-    // NetworkGeometry is no longer a subclass of Resource, but requires this method because, it is pure virtual.
-    assert(false);
-    return QSharedPointer<Resource>();
-}
+class GeometryReader : public QRunnable {
+public:
+    GeometryReader(QWeakPointer<Resource>& resource, const QUrl& url, const QVariantHash& mapping,
+        const QByteArray& data) :
+        _resource(resource), _url(url), _mapping(mapping), _data(data) {}
+    virtual ~GeometryReader() = default;
 
+    virtual void run() override;
 
-GeometryReader::GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping) :
-    _url(url),
-    _data(data),
-    _mapping(mapping) {
-}
+private:
+    QWeakPointer<Resource> _resource;
+    QUrl _url;
+    QVariantHash _mapping;
+    QByteArray _data;
+};
 
 void GeometryReader::run() {
     auto originalPriority = QThread::currentThread()->priority();
@@ -55,458 +103,353 @@ void GeometryReader::run() {
         originalPriority = QThread::NormalPriority;
     }
     QThread::currentThread()->setPriority(QThread::LowPriority);
+
+    // Ensure the resource is still being requested
+    auto resource = _resource.toStrongRef();
+    if (!resource) {
+        qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
+        return;
+    }
+
     try {
         if (_data.isEmpty()) {
-            throw QString("Reply is NULL ?!");
+            throw QString("reply is NULL");
         }
-        QString urlname = _url.path().toLower();
-        bool urlValid = true;
-        urlValid &= !urlname.isEmpty();
-        urlValid &= !_url.path().isEmpty();
-        urlValid &= _url.path().toLower().endsWith(".fbx") || _url.path().toLower().endsWith(".obj");
 
-        if (urlValid) {
-            // Let's read the binaries from the network
-            FBXGeometry* fbxgeo = nullptr;
+        QString urlname = _url.path().toLower();
+        if (!urlname.isEmpty() && !_url.path().isEmpty() &&
+            (_url.path().toLower().endsWith(".fbx") || _url.path().toLower().endsWith(".obj"))) {
+            FBXGeometry* fbxGeometry = nullptr;
+
             if (_url.path().toLower().endsWith(".fbx")) {
-                const bool grabLightmaps = true;
-                const float lightmapLevel = 1.0f;
-                fbxgeo = readFBX(_data, _mapping, _url.path(), grabLightmaps, lightmapLevel);
-                if (fbxgeo->meshes.size() == 0 && fbxgeo->joints.size() == 0) {
-                    // empty fbx geometry, indicates error
+                fbxGeometry = readFBX(_data, _mapping, _url.path());
+                if (fbxGeometry->meshes.size() == 0 && fbxGeometry->joints.size() == 0) {
                     throw QString("empty geometry, possibly due to an unsupported FBX version");
                 }
             } else if (_url.path().toLower().endsWith(".obj")) {
-                fbxgeo = OBJReader().readOBJ(_data, _mapping, _url);
+                fbxGeometry = OBJReader().readOBJ(_data, _mapping, _url);
             } else {
-                QString errorStr("unsupported format");
-                throw errorStr;
+                throw QString("unsupported format");
             }
-            emit onSuccess(fbxgeo);
+
+            QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition",
+                Q_ARG(void*, fbxGeometry));
         } else {
             throw QString("url is invalid");
         }
-
     } catch (const QString& error) {
         qCDebug(modelnetworking) << "Error reading " << _url << ": " << error;
-        emit onError(NetworkGeometry::ModelParseError, error);
+        QMetaObject::invokeMethod(resource.data(), "finishedLoading", Q_ARG(bool, false));
     }
 
     QThread::currentThread()->setPriority(originalPriority);
 }
 
-NetworkGeometry::NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl) :
-    _url(url),
-    _mapping(mapping),
-    _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url) {
+class GeometryDefinitionResource : public GeometryResource {
+    Q_OBJECT
+public:
+    GeometryDefinitionResource(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) :
+        GeometryResource(url), _mapping(mapping), _textureBaseUrl(textureBaseUrl) {}
 
-    if (delayLoad) {
-        _state = DelayState;
-    } else {
-        attemptRequestInternal();
-    }
+    virtual void downloadFinished(const QByteArray& data) override;
+
+protected:
+    Q_INVOKABLE void setGeometryDefinition(void* fbxGeometry);
+
+private:
+    QVariantHash _mapping;
+    QUrl _textureBaseUrl;
+};
+
+void GeometryDefinitionResource::downloadFinished(const QByteArray& data) {
+    QThreadPool::globalInstance()->start(new GeometryReader(_self, _url, _mapping, data));
 }
 
-NetworkGeometry::~NetworkGeometry() {
-    if (_resource) {
-        _resource->deleteLater();
-    }
-}
+void GeometryDefinitionResource::setGeometryDefinition(void* fbxGeometry) {
+    // Assume ownership of the geometry pointer
+    _geometry.reset(static_cast<FBXGeometry*>(fbxGeometry));
 
-void NetworkGeometry::attemptRequest() {
-    if (_state == DelayState) {
-        attemptRequestInternal();
-    }
-}
-
-void NetworkGeometry::attemptRequestInternal() {
-    if (_url.path().toLower().endsWith(".fst")) {
-        _mappingUrl = _url;
-        requestMapping(_url);
-    } else {
-        _modelUrl = _url;
-        requestModel(_url);
-    }
-}
-
-bool NetworkGeometry::isLoaded() const {
-    return _state == SuccessState;
-}
-
-bool NetworkGeometry::isLoadedWithTextures() const {
-    if (!isLoaded()) {
-        return false;
+    // Copy materials
+    QHash<QString, size_t> materialIDAtlas;
+    for (const FBXMaterial& material : _geometry->materials) {
+        materialIDAtlas[material.materialID] = _materials.size();
+        _materials.push_back(std::make_shared<NetworkMaterial>(material, _textureBaseUrl));
     }
 
-    if (!_isLoadedWithTextures) {
-        _hasTransparentTextures = false;
-
-        for (auto&& material : _materials) {
-            if ((material->albedoTexture && !material->albedoTexture->isLoaded()) ||
-                (material->normalTexture && !material->normalTexture->isLoaded()) ||
-                (material->roughnessTexture && !material->roughnessTexture->isLoaded()) ||
-                (material->metallicTexture && !material->metallicTexture->isLoaded()) ||
-                (material->occlusionTexture && !material->occlusionTexture->isLoaded()) ||
-                (material->emissiveTexture && !material->emissiveTexture->isLoaded()) ||
-                (material->lightmapTexture && !material->lightmapTexture->isLoaded())) {
-                return false;
-            }
-            if (material->albedoTexture && material->albedoTexture->getGPUTexture()) {
-                // Reassign the texture to make sure that itsalbedo alpha channel material key is detected correctly
-                material->_material->setTextureMap(model::MaterialKey::ALBEDO_MAP, material->_material->getTextureMap(model::MaterialKey::ALBEDO_MAP));
-                const auto& usage = material->albedoTexture->getGPUTexture()->getUsage();
-                bool isTransparentTexture = usage.isAlpha() && !usage.isAlphaMask();
-                _hasTransparentTextures |= isTransparentTexture;
-            }
-        }
-
-        _isLoadedWithTextures = true;
-    }
-    return true;
-}
-
-void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& url) {
-    if (_meshes.size() > 0) {
-        auto textureCache = DependencyManager::get<TextureCache>();
-        for (auto&& material : _materials) {
-            auto networkMaterial = material->_material;
-            auto oldTextureMaps = networkMaterial->getTextureMaps();
-            if (material->albedoTextureName == name) {
-                material->albedoTexture = textureCache->getTexture(url, DEFAULT_TEXTURE);
-
-                auto albedoMap = model::TextureMapPointer(new model::TextureMap());
-                albedoMap->setTextureSource(material->albedoTexture->_textureSource);
-                albedoMap->setTextureTransform(oldTextureMaps[model::MaterialKey::ALBEDO_MAP]->getTextureTransform());
-                // when reassigning the albedo texture we also check for the alpha channel used as opacity
-                albedoMap->setUseAlphaChannel(true); 
-                networkMaterial->setTextureMap(model::MaterialKey::ALBEDO_MAP, albedoMap);
-            } else if (material->normalTextureName == name) {
-                material->normalTexture = textureCache->getTexture(url);
-
-                auto normalMap = model::TextureMapPointer(new model::TextureMap());
-                normalMap->setTextureSource(material->normalTexture->_textureSource);
-
-                networkMaterial->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap);
-            } else if (material->roughnessTextureName == name) {
-                // FIXME: If passing a gloss map instead of a roughmap how to say that ? looking for gloss in the name ?
-                material->roughnessTexture = textureCache->getTexture(url, ROUGHNESS_TEXTURE);
-
-                auto roughnessMap = model::TextureMapPointer(new model::TextureMap());
-                roughnessMap->setTextureSource(material->roughnessTexture->_textureSource);
-
-                networkMaterial->setTextureMap(model::MaterialKey::ROUGHNESS_MAP, roughnessMap);
-            } else if (material->metallicTextureName == name) {
-                // FIXME: If passing a specular map instead of a metallic how to say that ? looking for wtf in the name ?
-                material->metallicTexture = textureCache->getTexture(url, METALLIC_TEXTURE);
-
-                auto glossMap = model::TextureMapPointer(new model::TextureMap());
-                glossMap->setTextureSource(material->metallicTexture->_textureSource);
-
-                networkMaterial->setTextureMap(model::MaterialKey::METALLIC_MAP, glossMap);
-            } else if (material->emissiveTextureName == name) {
-                material->emissiveTexture = textureCache->getTexture(url, EMISSIVE_TEXTURE);
-
-                auto emissiveMap = model::TextureMapPointer(new model::TextureMap());
-                emissiveMap->setTextureSource(material->emissiveTexture->_textureSource);
-
-                networkMaterial->setTextureMap(model::MaterialKey::EMISSIVE_MAP, emissiveMap);
-            } else if (material->lightmapTextureName == name) {
-                material->lightmapTexture = textureCache->getTexture(url, LIGHTMAP_TEXTURE);
-
-                auto lightmapMap = model::TextureMapPointer(new model::TextureMap());
-                lightmapMap->setTextureSource(material->lightmapTexture->_textureSource);
-                lightmapMap->setTextureTransform(
-                    oldTextureMaps[model::MaterialKey::LIGHTMAP_MAP]->getTextureTransform());
-                glm::vec2 oldOffsetScale =
-                    oldTextureMaps[model::MaterialKey::LIGHTMAP_MAP]->getLightmapOffsetScale();
-                lightmapMap->setLightmapOffsetScale(oldOffsetScale.x, oldOffsetScale.y);
-
-                networkMaterial->setTextureMap(model::MaterialKey::LIGHTMAP_MAP, lightmapMap);
-            }
-        }
-    } else {
-        qCWarning(modelnetworking) << "Ignoring setTextureWithNameToURL() geometry not ready." << name << url;
-    }
-    _isLoadedWithTextures = false;
-}
-
-QStringList NetworkGeometry::getTextureNames() const {
-    QStringList result;
-    for (auto&& material : _materials) {
-        if (!material->emissiveTextureName.isEmpty() && material->emissiveTexture) {
-            QString textureURL = material->emissiveTexture->getURL().toString();
-            result << material->emissiveTextureName + ":\"" + textureURL + "\"";
-        }
-
-        if (!material->albedoTextureName.isEmpty() && material->albedoTexture) {
-            QString textureURL = material->albedoTexture->getURL().toString();
-            result << material->albedoTextureName + ":\"" + textureURL + "\"";
-        }
-
-        if (!material->normalTextureName.isEmpty() && material->normalTexture) {
-            QString textureURL = material->normalTexture->getURL().toString();
-            result << material->normalTextureName + ":\"" + textureURL + "\"";
-        }
-
-        if (!material->roughnessTextureName.isEmpty() && material->roughnessTexture) {
-            QString textureURL = material->roughnessTexture->getURL().toString();
-            result << material->roughnessTextureName + ":\"" + textureURL + "\"";
-        }
-
-        if (!material->metallicTextureName.isEmpty() && material->metallicTexture) {
-            QString textureURL = material->metallicTexture->getURL().toString();
-            result << material->metallicTextureName + ":\"" + textureURL + "\"";
-        }
-
-        if (!material->occlusionTextureName.isEmpty() && material->occlusionTexture) {
-            QString textureURL = material->occlusionTexture->getURL().toString();
-            result << material->occlusionTextureName + ":\"" + textureURL + "\"";
-        }
-
-        if (!material->lightmapTextureName.isEmpty() && material->lightmapTexture) {
-            QString textureURL = material->lightmapTexture->getURL().toString();
-            result << material->lightmapTextureName + ":\"" + textureURL + "\"";
-        }
-    }
-
-    return result;
-}
-
-void NetworkGeometry::requestMapping(const QUrl& url) {
-    _state = RequestMappingState;
-    if (_resource) {
-        _resource->deleteLater();
-    }
-    _resource = new Resource(url, false);
-    connect(_resource, &Resource::loaded, this, &NetworkGeometry::mappingRequestDone);
-    connect(_resource, &Resource::failed, this, &NetworkGeometry::mappingRequestError);
-}
-
-void NetworkGeometry::requestModel(const QUrl& url) {
-    _state = RequestModelState;
-    if (_resource) {
-        _resource->deleteLater();
-    }
-    _modelUrl = url;
-    _resource = new Resource(url, false);
-    connect(_resource, &Resource::loaded, this, &NetworkGeometry::modelRequestDone);
-    connect(_resource, &Resource::failed, this, &NetworkGeometry::modelRequestError);
-}
-
-void NetworkGeometry::mappingRequestDone(const QByteArray& data) {
-    assert(_state == RequestMappingState);
-
-    // parse the mapping file
-    _mapping = FSTReader::readMapping(data);
-
-    QUrl replyUrl = _mappingUrl;
-    QString modelUrlStr = _mapping.value("filename").toString();
-    if (modelUrlStr.isNull()) {
-        qCDebug(modelnetworking) << "Mapping file " << _url << "has no \"filename\" entry";
-        emit onFailure(*this, MissingFilenameInMapping);
-    } else {
-        // read _textureBase from mapping file, if present
-        QString texdir = _mapping.value("texdir").toString();
-        if (!texdir.isNull()) {
-            if (!texdir.endsWith('/')) {
-                texdir += '/';
-            }
-            _textureBaseUrl = replyUrl.resolved(texdir);
-        }
-
-        _modelUrl = replyUrl.resolved(modelUrlStr);
-        requestModel(_modelUrl);
-    }
-}
-
-void NetworkGeometry::mappingRequestError(QNetworkReply::NetworkError error) {
-    assert(_state == RequestMappingState);
-    _state = ErrorState;
-    emit onFailure(*this, MappingRequestError);
-}
-
-void NetworkGeometry::modelRequestDone(const QByteArray& data) {
-    assert(_state == RequestModelState);
-
-    _state = ParsingModelState;
-
-    // asynchronously parse the model file.
-    GeometryReader* geometryReader = new GeometryReader(_modelUrl, data, _mapping);
-    connect(geometryReader, SIGNAL(onSuccess(FBXGeometry*)), SLOT(modelParseSuccess(FBXGeometry*)));
-    connect(geometryReader, SIGNAL(onError(int, QString)), SLOT(modelParseError(int, QString)));
-
-    QThreadPool::globalInstance()->start(geometryReader);
-}
-
-void NetworkGeometry::modelRequestError(QNetworkReply::NetworkError error) {
-    assert(_state == RequestModelState);
-    _state = ErrorState;
-    emit onFailure(*this, ModelRequestError);
-}
-
-static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBaseUrl) {
-    NetworkMesh* networkMesh = new NetworkMesh();
-
-    networkMesh->_mesh = mesh._mesh;
-
-    return networkMesh;
-}
-
-
-static model::TextureMapPointer setupNetworkTextureMap(NetworkGeometry* geometry, const QUrl& textureBaseUrl,
-        const FBXTexture& texture, TextureType type,
-        NetworkTexturePointer& networkTexture, QString& networkTextureName) {
-    auto textureCache = DependencyManager::get<TextureCache>();
-
-    // If content is inline, cache it under the fbx file, not its base url
-    const auto baseUrl = texture.content.isEmpty() ? textureBaseUrl : QUrl(textureBaseUrl.url() + "/");
-    const auto filename = baseUrl.resolved(QUrl(texture.filename));
-
-    networkTexture = textureCache->getTexture(filename, type, texture.content);
-    QObject::connect(networkTexture.data(), &NetworkTexture::networkTextureCreated, geometry, &NetworkGeometry::textureLoaded);
-    networkTextureName = texture.name;
-
-    auto map = std::make_shared<model::TextureMap>();
-    map->setTextureSource(networkTexture->_textureSource);
-    return map;
-}
-
-static NetworkMaterial* buildNetworkMaterial(NetworkGeometry* geometry, const FBXMaterial& material, const QUrl& textureBaseUrl) {
-    NetworkMaterial* networkMaterial = new NetworkMaterial();
-    networkMaterial->_material = material._material;
-
-    if (!material.albedoTexture.filename.isEmpty()) {
-        auto albedoMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.albedoTexture, DEFAULT_TEXTURE,
-            networkMaterial->albedoTexture, networkMaterial->albedoTextureName);
-        albedoMap->setTextureTransform(material.albedoTexture.transform);
-
-        if (!material.opacityTexture.filename.isEmpty()) {
-            if (material.albedoTexture.filename == material.opacityTexture.filename) {
-                // Best case scenario, just indicating that the albedo map contains transparency
-                albedoMap->setUseAlphaChannel(true);
-            } else {
-                // Opacity Map is different from the Abledo map, not supported
-            }
-        }
-
-        material._material->setTextureMap(model::MaterialKey::ALBEDO_MAP, albedoMap);
-    }
-
-
-    if (!material.normalTexture.filename.isEmpty()) {
-        auto normalMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.normalTexture,
-            (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE),
-            networkMaterial->normalTexture, networkMaterial->normalTextureName);
-        networkMaterial->_material->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap);
-    }
-
-    // Roughness first or gloss maybe
-    if (!material.roughnessTexture.filename.isEmpty()) {
-        auto roughnessMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.roughnessTexture, ROUGHNESS_TEXTURE,
-            networkMaterial->roughnessTexture, networkMaterial->roughnessTextureName);
-        material._material->setTextureMap(model::MaterialKey::ROUGHNESS_MAP, roughnessMap);
-    } else if (!material.glossTexture.filename.isEmpty()) {
-        auto roughnessMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.glossTexture, GLOSS_TEXTURE,
-            networkMaterial->roughnessTexture, networkMaterial->roughnessTextureName);
-        material._material->setTextureMap(model::MaterialKey::ROUGHNESS_MAP, roughnessMap);
-    }
-
-    // Metallic first or specular maybe
-
-    if (!material.metallicTexture.filename.isEmpty()) {
-        auto metallicMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.metallicTexture, METALLIC_TEXTURE,
-            networkMaterial->metallicTexture, networkMaterial->metallicTextureName);
-        material._material->setTextureMap(model::MaterialKey::METALLIC_MAP, metallicMap);
-    } else if (!material.specularTexture.filename.isEmpty()) {
-
-        auto metallicMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.specularTexture, SPECULAR_TEXTURE,
-            networkMaterial->metallicTexture, networkMaterial->metallicTextureName);
-        material._material->setTextureMap(model::MaterialKey::METALLIC_MAP, metallicMap);
-    }
-
-    if (!material.occlusionTexture.filename.isEmpty()) {
-        auto occlusionMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.occlusionTexture, OCCLUSION_TEXTURE,
-            networkMaterial->occlusionTexture, networkMaterial->occlusionTextureName);
-        material._material->setTextureMap(model::MaterialKey::OCCLUSION_MAP, occlusionMap);
-    }
-
-    if (!material.emissiveTexture.filename.isEmpty()) {
-        auto emissiveMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.emissiveTexture, EMISSIVE_TEXTURE,
-            networkMaterial->emissiveTexture, networkMaterial->emissiveTextureName);
-        material._material->setTextureMap(model::MaterialKey::EMISSIVE_MAP, emissiveMap);
-    }
-
-    if (!material.lightmapTexture.filename.isEmpty()) {
-        auto lightmapMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.lightmapTexture, LIGHTMAP_TEXTURE,
-            networkMaterial->lightmapTexture, networkMaterial->lightmapTextureName);
-        lightmapMap->setTextureTransform(material.lightmapTexture.transform);
-        lightmapMap->setLightmapOffsetScale(material.lightmapParams.x, material.lightmapParams.y);
-        material._material->setTextureMap(model::MaterialKey::LIGHTMAP_MAP, lightmapMap);
-    }
-
-    return networkMaterial;
-}
-
-
-void NetworkGeometry::modelParseSuccess(FBXGeometry* geometry) {
-    // assume owner ship of geometry pointer
-    _geometry.reset(geometry);
-
-
-
-    foreach(const FBXMesh& mesh, _geometry->meshes) {
-        _meshes.emplace_back(buildNetworkMesh(mesh, _textureBaseUrl));
-    }
-
-    QHash<QString, size_t> fbxMatIDToMatID;
-    foreach(const FBXMaterial& material, _geometry->materials) {
-        fbxMatIDToMatID[material.materialID] = _materials.size();
-        _materials.emplace_back(buildNetworkMaterial(this, material, _textureBaseUrl));
-    }
-
-
+    std::shared_ptr<NetworkMeshes> meshes = std::make_shared<NetworkMeshes>();
+    std::shared_ptr<NetworkShapes> shapes = std::make_shared<NetworkShapes>();
     int meshID = 0;
-    foreach(const FBXMesh& mesh, _geometry->meshes) {
+    for (const FBXMesh& mesh : _geometry->meshes) {
+        // Copy mesh pointers
+        meshes->emplace_back(mesh._mesh);
         int partID = 0;
-        foreach (const FBXMeshPart& part, mesh.parts) {
-            NetworkShape* networkShape = new NetworkShape();
-            networkShape->_meshID = meshID;
-            networkShape->_partID = partID;
-            networkShape->_materialID = (int)fbxMatIDToMatID[part.materialID];
-            _shapes.emplace_back(networkShape);
+        for (const FBXMeshPart& part : mesh.parts) {
+            // Construct local shapes
+            shapes->emplace_back(meshID, partID, (int)materialIDAtlas[part.materialID]);
             partID++;
         }
         meshID++;
     }
+    _meshes = meshes;
+    _shapes = shapes;
 
-    _state = SuccessState;
-    emit onSuccess(*this, *_geometry.get());
-
-    delete _resource;
-    _resource = nullptr;
+    finishedLoading(true);
 }
 
-void NetworkGeometry::modelParseError(int error, QString str) {
-    _state = ErrorState;
-    emit onFailure(*this, (NetworkGeometry::Error)error);
-
-    delete _resource;
-    _resource = nullptr;
+ModelCache::ModelCache() {
+    const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE;
+    setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE);
 }
 
-const NetworkMaterial* NetworkGeometry::getShapeMaterial(int shapeID) {
-    if ((shapeID >= 0) && (shapeID < (int)_shapes.size())) {
-        int materialID = _shapes[shapeID]->_materialID;
-        if ((materialID >= 0) && ((unsigned int)materialID < _materials.size())) {
-            return _materials[materialID].get();
-        } else {
-            return 0;
+QSharedPointer<Resource> ModelCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
+                                                    bool delayLoad, const void* extra) {
+    const GeometryExtra* geometryExtra = static_cast<const GeometryExtra*>(extra);
+
+    Resource* resource = nullptr;
+    if (url.path().toLower().endsWith(".fst")) {
+        resource = new GeometryMappingResource(url);
+    } else {
+        resource = new GeometryDefinitionResource(url, geometryExtra->mapping, geometryExtra->textureBaseUrl);
+    }
+
+    return QSharedPointer<Resource>(resource, &Resource::allReferencesCleared);
+}
+
+std::shared_ptr<NetworkGeometry> ModelCache::getGeometry(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) {
+    GeometryExtra geometryExtra = { mapping, textureBaseUrl };
+    GeometryResource::Pointer resource = getResource(url, QUrl(), true, &geometryExtra).staticCast<GeometryResource>();
+    return std::make_shared<NetworkGeometry>(resource);
+}
+
+const QVariantMap Geometry::getTextures() const {
+    QVariantMap textures;
+    for (const auto& material : _materials) {
+        for (const auto& texture : material->_textures) {
+            if (texture.texture) {
+                textures[texture.name] = texture.texture->getURL();
+            }
+        }
+    }
+
+    return textures;
+}
+
+void Geometry::setTextures(const QVariantMap& textureMap) {
+    if (_meshes->size() > 0) {
+        for (auto& material : _materials) {
+            // Check if any material textures actually changed
+            if (std::any_of(material->_textures.cbegin(), material->_textures.cend(),
+                [&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.texture && textureMap.contains(it.name); })) { 
+
+                material = std::make_shared<NetworkMaterial>(*material, textureMap);
+                _areTexturesLoaded = false;
+            }
         }
     } else {
-        return 0;
+        qCWarning(modelnetworking) << "Ignoring setTextures(); geometry not ready";
     }
 }
 
-void NetworkGeometry::textureLoaded(const QWeakPointer<NetworkTexture>& networkTexture) {
-    numTextureLoaded++;
+bool Geometry::areTexturesLoaded() const {
+    if (!_areTexturesLoaded) {
+        _hasTransparentTextures = false;
+
+        for (auto& material : _materials) {
+            // Check if material textures are loaded
+            if (std::any_of(material->_textures.cbegin(), material->_textures.cend(),
+                [](const NetworkMaterial::Textures::value_type& it) { return it.texture && !it.texture->isLoaded(); })) {
+
+                return false;
+            }
+
+            // If material textures are loaded, check the material translucency
+            const auto albedoTexture = material->_textures[NetworkMaterial::MapChannel::ALBEDO_MAP];
+            if (albedoTexture.texture && albedoTexture.texture->getGPUTexture()) {
+                material->resetOpacityMap();
+
+                _hasTransparentTextures |= material->getKey().isTranslucent();
+            }
+        }
+
+        _areTexturesLoaded = true;
+    }
+    return true;
 }
+
+const std::shared_ptr<const NetworkMaterial> Geometry::getShapeMaterial(int shapeID) const {
+    if ((shapeID >= 0) && (shapeID < (int)_shapes->size())) {
+        int materialID = _shapes->at(shapeID).materialID;
+        if ((materialID >= 0) && (materialID < (int)_materials.size())) {
+            return _materials[materialID];
+        }
+    }
+    return nullptr;
+}
+
+NetworkGeometry::NetworkGeometry(const GeometryResource::Pointer& networkGeometry) : _resource(networkGeometry) {
+    connect(_resource.data(), &Resource::finished, this, &NetworkGeometry::resourceFinished);
+    connect(_resource.data(), &Resource::onRefresh, this, &NetworkGeometry::resourceRefreshed);
+    if (_resource->isLoaded()) {
+        resourceFinished();
+    }
+}
+
+void NetworkGeometry::resourceFinished() {
+    _instance = std::make_shared<Geometry>(*_resource);
+}
+
+void NetworkGeometry::resourceRefreshed() {
+    _instance.reset();
+}
+
+const QString NetworkMaterial::NO_TEXTURE = QString();
+
+const QString& NetworkMaterial::getTextureName(MapChannel channel) {
+    if (_textures[channel].texture) {
+        return _textures[channel].name;
+    }
+    return NO_TEXTURE;
+}
+
+QUrl NetworkMaterial::getTextureUrl(const QUrl& url, const FBXTexture& texture) {
+    // If content is inline, cache it under the fbx file, not its url
+    const auto baseUrl = texture.content.isEmpty() ? url: QUrl(url.url() + "/");
+    return baseUrl.resolved(QUrl(texture.filename));
+}
+
+model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture,
+                                                        TextureType type, MapChannel channel) {
+    const auto url = getTextureUrl(baseUrl, fbxTexture);
+    const auto texture = DependencyManager::get<TextureCache>()->getTexture(url, type, fbxTexture.content);
+    _textures[channel] = Texture { fbxTexture.name, texture };
+
+    auto map = std::make_shared<model::TextureMap>();
+    map->setTextureSource(texture->_textureSource);
+    return map;
+}
+
+model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, TextureType type, MapChannel channel) {
+    const auto texture = DependencyManager::get<TextureCache>()->getTexture(url, type);
+    _textures[channel].texture = texture;
+
+    auto map = std::make_shared<model::TextureMap>();
+    map->setTextureSource(texture->_textureSource);
+    return map;
+}
+
+NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) {
+    _textures = Textures(MapChannel::NUM_MAP_CHANNELS);
+    if (!material.albedoTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, DEFAULT_TEXTURE, MapChannel::ALBEDO_MAP);
+        map->setTextureTransform(material.albedoTexture.transform);
+
+        if (!material.opacityTexture.filename.isEmpty()) {
+            if (material.albedoTexture.filename == material.opacityTexture.filename) {
+                // Best case scenario, just indicating that the albedo map contains transparency
+                // TODO: Different albedo/opacity maps are not currently supported
+                map->setUseAlphaChannel(true);
+            }
+        }
+
+        setTextureMap(MapChannel::ALBEDO_MAP, map);
+    }
+
+
+    if (!material.normalTexture.filename.isEmpty()) {
+        auto type = (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE);
+        auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP);
+        setTextureMap(MapChannel::NORMAL_MAP, map);
+    }
+
+    if (!material.roughnessTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP);
+        setTextureMap(MapChannel::ROUGHNESS_MAP, map);
+    } else if (!material.glossTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP);
+        setTextureMap(MapChannel::ROUGHNESS_MAP, map);
+    }
+
+    if (!material.metallicTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, METALLIC_TEXTURE, MapChannel::METALLIC_MAP);
+        setTextureMap(MapChannel::METALLIC_MAP, map);
+    } else if (!material.specularTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, SPECULAR_TEXTURE, MapChannel::METALLIC_MAP);
+        setTextureMap(MapChannel::METALLIC_MAP, map);
+    }
+
+    if (!material.occlusionTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP);
+        setTextureMap(MapChannel::OCCLUSION_MAP, map);
+    }
+
+    if (!material.emissiveTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP);
+        setTextureMap(MapChannel::EMISSIVE_MAP, map);
+    }
+
+    if (!material.lightmapTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP);
+        map->setTextureTransform(material.lightmapTexture.transform);
+        map->setLightmapOffsetScale(material.lightmapParams.x, material.lightmapParams.y);
+        setTextureMap(MapChannel::LIGHTMAP_MAP, map);
+    }
+}
+
+NetworkMaterial::NetworkMaterial(const NetworkMaterial& material, const QVariantMap& textureMap) : NetworkMaterial(material) {
+    _textures = material._textures;
+
+    const auto& albedoName = getTextureName(MapChannel::ALBEDO_MAP);
+    const auto& normalName = getTextureName(MapChannel::NORMAL_MAP);
+    const auto& roughnessName = getTextureName(MapChannel::ROUGHNESS_MAP);
+    const auto& metallicName = getTextureName(MapChannel::METALLIC_MAP);
+    const auto& occlusionName = getTextureName(MapChannel::OCCLUSION_MAP);
+    const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP);
+    const auto& lightmapName = getTextureName(MapChannel::LIGHTMAP_MAP);
+
+    if (!albedoName.isEmpty() && textureMap.contains(albedoName)) {
+        auto map = fetchTextureMap(textureMap[albedoName].toUrl(), DEFAULT_TEXTURE, MapChannel::ALBEDO_MAP);
+        map->setTextureTransform(getTextureMap(MapChannel::ALBEDO_MAP)->getTextureTransform());
+        // when reassigning the albedo texture we also check for the alpha channel used as opacity
+        map->setUseAlphaChannel(true); 
+        setTextureMap(MapChannel::ALBEDO_MAP, map);
+    }
+
+    if (!normalName.isEmpty() && textureMap.contains(normalName)) {
+        auto map = fetchTextureMap(textureMap[normalName].toUrl(), DEFAULT_TEXTURE, MapChannel::NORMAL_MAP);
+        setTextureMap(MapChannel::NORMAL_MAP, map);
+    }
+
+    if (!roughnessName.isEmpty() && textureMap.contains(roughnessName)) {
+        // FIXME: If passing a gloss map instead of a roughmap how do we know?
+        auto map = fetchTextureMap(textureMap[roughnessName].toUrl(), ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP);
+        setTextureMap(MapChannel::ROUGHNESS_MAP, map);
+    }
+
+    if (!metallicName.isEmpty() && textureMap.contains(metallicName)) {
+        // FIXME: If passing a specular map instead of a metallic how do we know?
+        auto map = fetchTextureMap(textureMap[metallicName].toUrl(), METALLIC_TEXTURE, MapChannel::METALLIC_MAP);
+        setTextureMap(MapChannel::METALLIC_MAP, map);
+    }
+
+    if (!occlusionName.isEmpty() && textureMap.contains(occlusionName)) {
+        auto map = fetchTextureMap(textureMap[occlusionName].toUrl(), OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP);
+        setTextureMap(MapChannel::OCCLUSION_MAP, map);
+    }
+
+    if (!emissiveName.isEmpty() && textureMap.contains(emissiveName)) {
+        auto map = fetchTextureMap(textureMap[emissiveName].toUrl(), EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP);
+        setTextureMap(MapChannel::EMISSIVE_MAP, map);
+    }
+
+    if (!lightmapName.isEmpty() && textureMap.contains(lightmapName)) {
+        auto map = fetchTextureMap(textureMap[lightmapName].toUrl(), LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP);
+        auto oldMap = getTextureMap(MapChannel::LIGHTMAP_MAP);
+        map->setTextureTransform(oldMap->getTextureTransform());
+        glm::vec2 offsetScale = oldMap->getLightmapOffsetScale();
+        map->setLightmapOffsetScale(offsetScale.x, offsetScale.y);
+        setTextureMap(MapChannel::LIGHTMAP_MAP, map);
+    }
+}
+
+#include "ModelCache.moc"
+
diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h
index 1c76a0b878..a2fcc9d741 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.h
+++ b/libraries/model-networking/src/model-networking/ModelCache.h
@@ -1,9 +1,9 @@
 //
 //  ModelCache.h
-//  libraries/model-networking/src/model-networking
+//  libraries/model-networking
 //
-//  Created by Sam Gateau on 9/21/15.
-//  Copyright 2013 High Fidelity, Inc.
+//  Created by Zach Pomerantz on 3/15/16.
+//  Copyright 2016 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
@@ -12,200 +12,156 @@
 #ifndef hifi_ModelCache_h
 #define hifi_ModelCache_h
 
-#include <QMap>
-#include <QRunnable>
-
 #include <DependencyManager.h>
 #include <ResourceCache.h>
 
-#include "FBXReader.h"
-#include "OBJReader.h"
-
-#include <gpu/Batch.h>
-#include <gpu/Stream.h>
-
-
 #include <model/Material.h>
 #include <model/Asset.h>
 
-class NetworkGeometry;
-class NetworkMesh;
+#include "FBXReader.h"
+#include "TextureCache.h"
+
+// Alias instead of derive to avoid copying
+using NetworkMesh = model::Mesh;
+
 class NetworkTexture;
 class NetworkMaterial;
 class NetworkShape;
+class NetworkGeometry;
 
-/// Stores cached geometry.
+class GeometryMappingResource;
+
+/// Stores cached model geometries.
 class ModelCache : public ResourceCache, public Dependency {
     Q_OBJECT
     SINGLETON_DEPENDENCY
 
 public:
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-                                                    bool delayLoad, const void* extra);
+    /// Loads a model geometry from the specified URL.
+    std::shared_ptr<NetworkGeometry> getGeometry(const QUrl& url,
+        const QVariantHash& mapping = QVariantHash(), const QUrl& textureBaseUrl = QUrl());
 
-    /// Loads geometry from the specified URL.
-    /// \param fallback a fallback URL to load if the desired one is unavailable
-    /// \param delayLoad if true, don't load the geometry immediately; wait until load is first requested
-    QSharedPointer<NetworkGeometry> getGeometry(const QUrl& url, const QUrl& fallback = QUrl(), bool delayLoad = false);
+protected:
+    friend class GeometryMappingResource;
+
+    virtual QSharedPointer<Resource> createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra);
 
 private:
     ModelCache();
-    virtual ~ModelCache();
+    virtual ~ModelCache() = default;
+};
 
-    QHash<QUrl, QWeakPointer<NetworkGeometry> > _networkGeometry;
+class Geometry {
+public:
+    using Pointer = std::shared_ptr<Geometry>;
+
+    // Immutable over lifetime
+    using NetworkMeshes = std::vector<std::shared_ptr<const NetworkMesh>>;
+    using NetworkShapes = std::vector<NetworkShape>;
+
+    // Mutable, but must retain structure of vector
+    using NetworkMaterials = std::vector<std::shared_ptr<const NetworkMaterial>>;
+
+    const FBXGeometry& getGeometry() const { return *_geometry; }
+    const NetworkMeshes& getMeshes() const { return *_meshes; }
+    const std::shared_ptr<const NetworkMaterial> getShapeMaterial(int shapeID) const;
+
+    const QVariantMap getTextures() const;
+    void setTextures(const QVariantMap& textureMap);
+
+    virtual bool areTexturesLoaded() const;
+    // Returns true if any albedo texture has a non-masking alpha channel.
+    // This can only be known after areTexturesLoaded().
+    bool hasTransparentTextures() const { return _hasTransparentTextures; }
+
+protected:
+    friend class GeometryMappingResource;
+
+    // Shared across all geometries, constant throughout lifetime
+    std::shared_ptr<const FBXGeometry> _geometry;
+    std::shared_ptr<const NetworkMeshes> _meshes;
+    std::shared_ptr<const NetworkShapes> _shapes;
+
+    // Copied to each geometry, mutable throughout lifetime via setTextures
+    NetworkMaterials _materials;
+
+private:
+    mutable bool _areTexturesLoaded { false };
+    mutable bool _hasTransparentTextures { false };
+};
+
+/// A geometry loaded from the network.
+class GeometryResource : public Resource, public Geometry {
+public:
+    using Pointer = QSharedPointer<GeometryResource>;
+
+    GeometryResource(const QUrl& url) : Resource(url) {}
+
+    virtual bool areTexturesLoaded() const { return isLoaded() && Geometry::areTexturesLoaded(); }
+
+protected:
+    virtual bool isCacheable() const override { return _loaded; }
 };
 
 class NetworkGeometry : public QObject {
     Q_OBJECT
-
 public:
-    // mapping is only used if url is a .fbx or .obj file, it is essentially the content of an fst file.
-    // if delayLoad is true, the url will not be immediately downloaded.
-    // use the attemptRequest method to initiate the download.
-    NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl = QUrl());
-    ~NetworkGeometry();
+    using Pointer = std::shared_ptr<NetworkGeometry>;
 
-    const QUrl& getURL() const { return _url; }
+    NetworkGeometry() = delete;
+    NetworkGeometry(const GeometryResource::Pointer& networkGeometry);
 
-    void attemptRequest();
+    const QUrl& getURL() { return _resource->getURL(); }
 
-    // true when the geometry is loaded (but maybe not it's associated textures)
-    bool isLoaded() const;
+    /// Returns the geometry, if it is loaded (must be checked!)
+    const Geometry::Pointer& getGeometry() { return _instance; }
 
-    // true when the requested geometry and its textures are loaded.
-    bool isLoadedWithTextures() const;
+private slots:
+    void resourceFinished();
+    void resourceRefreshed();
 
-    // true if the albedo texture has a non-masking alpha channel.
-    // This can only be known after isLoadedWithTextures().
-    bool hasTransparentTextures() const { return _hasTransparentTextures; }
+private:
+    GeometryResource::Pointer _resource;
+    Geometry::Pointer _instance { nullptr };
+};
 
-    // WARNING: only valid when isLoaded returns true.
-    const FBXGeometry& getFBXGeometry() const { return *_geometry; }
-    const std::vector<std::unique_ptr<NetworkMesh>>& getMeshes() const { return _meshes; }
-  //  const model::AssetPointer getAsset() const { return _asset; }
-
-   // model::MeshPointer getShapeMesh(int shapeID);
-   // int getShapePart(int shapeID);
-
-    // This would be the final verison
-    // model::MaterialPointer getShapeMaterial(int shapeID);
-    const NetworkMaterial* getShapeMaterial(int shapeID);
-
-
-    void setTextureWithNameToURL(const QString& name, const QUrl& url);
-    QStringList getTextureNames() const;
-
-    enum Error {
-        MissingFilenameInMapping = 0,
-        MappingRequestError,
-        ModelRequestError,
-        ModelParseError
-    };
-
-signals:
-    // Fired when everything has downloaded and parsed successfully.
-    void onSuccess(NetworkGeometry& networkGeometry, FBXGeometry& fbxGeometry);
-
-    // Fired when something went wrong.
-    void onFailure(NetworkGeometry& networkGeometry, Error error);
-
-public slots:
-    void textureLoaded(const QWeakPointer<NetworkTexture>& networkTexture);
-
-protected slots:
-    void mappingRequestDone(const QByteArray& data);
-    void mappingRequestError(QNetworkReply::NetworkError error);
-
-    void modelRequestDone(const QByteArray& data);
-    void modelRequestError(QNetworkReply::NetworkError error);
-
-    void modelParseSuccess(FBXGeometry* geometry);
-    void modelParseError(int error, QString str);
+class NetworkMaterial : public model::Material {
+public:
+    using MapChannel = model::Material::MapChannel;
 
+    NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl);
+    NetworkMaterial(const NetworkMaterial& material, const QVariantMap& textureMap);
 
 protected:
-    void attemptRequestInternal();
-    void requestMapping(const QUrl& url);
-    void requestModel(const QUrl& url);
+    friend class Geometry;
 
-    enum State { DelayState,
-                 RequestMappingState,
-                 RequestModelState,
-                 ParsingModelState,
-                 SuccessState,
-                 ErrorState };
-    State _state;
+    class Texture {
+    public:
+        QString name;
+        QSharedPointer<NetworkTexture> texture;
+    };
+    using Textures = std::vector<Texture>;
 
-    QUrl _url;
-    QUrl _mappingUrl;
-    QUrl _modelUrl;
-    QVariantHash _mapping;
-    QUrl _textureBaseUrl;
-    int numTextureLoaded = 0;
+    Textures _textures;
 
-    Resource* _resource = nullptr;
-    std::unique_ptr<FBXGeometry> _geometry; // This should go away evenutally once we can put everything we need in the model::AssetPointer
-    std::vector<std::unique_ptr<NetworkMesh>> _meshes;
-    std::vector<std::unique_ptr<NetworkMaterial>> _materials;
-    std::vector<std::unique_ptr<NetworkShape>> _shapes;
+    static const QString NO_TEXTURE;
+    const QString& getTextureName(MapChannel channel);
 
-
-    // The model asset created from this NetworkGeometry
-    // model::AssetPointer _asset;
-
-    // cache for isLoadedWithTextures()
-    mutable bool _isLoadedWithTextures = false;
-    mutable bool _hasTransparentTextures = false;
-};
-
-/// Reads geometry in a worker thread.
-class GeometryReader : public QObject, public QRunnable {
-    Q_OBJECT
-public:
-    GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping);
-    virtual void run();
-signals:
-    void onSuccess(FBXGeometry* geometry);
-    void onError(int error, QString str);
 private:
-    QUrl _url;
-    QByteArray _data;
-    QVariantHash _mapping;
+    // Helpers for the ctors
+    QUrl getTextureUrl(const QUrl& baseUrl, const FBXTexture& fbxTexture);
+    model::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture,
+        TextureType type, MapChannel channel);
+    model::TextureMapPointer fetchTextureMap(const QUrl& url, TextureType type, MapChannel channel);
 };
 
-
 class NetworkShape {
 public:
-    int _meshID{ -1 };
-    int _partID{ -1 };
-    int _materialID{ -1 };
+    NetworkShape(int mesh, int part, int material) : meshID { mesh }, partID { part }, materialID { material } {}
+    int meshID { -1 };
+    int partID { -1 };
+    int materialID { -1 };
 };
 
-class NetworkMaterial {
-public:
-
-    model::MaterialPointer _material;
-    QString emissiveTextureName;
-    QSharedPointer<NetworkTexture> emissiveTexture;
-    QString albedoTextureName;
-    QSharedPointer<NetworkTexture> albedoTexture;
-    QString normalTextureName;
-    QSharedPointer<NetworkTexture> normalTexture;
-    QString roughnessTextureName;
-    QSharedPointer<NetworkTexture> roughnessTexture;
-    QString metallicTextureName;
-    QSharedPointer<NetworkTexture> metallicTexture;
-    QString occlusionTextureName;
-    QSharedPointer<NetworkTexture> occlusionTexture;
-    QString lightmapTextureName;
-    QSharedPointer<NetworkTexture> lightmapTexture;
-};
-
-
-/// The state associated with a single mesh.
-class NetworkMesh {
-public:
-    model::MeshPointer _mesh;
-};
-
-#endif // hifi_GeometryCache_h
+#endif // hifi_ModelCache_h