diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
index bcc2a2821c..dc892e6640 100644
--- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
+++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml
@@ -607,7 +607,7 @@ Rectangle {
                         } else if (msg.method === "showTrashLightbox") {
                             lightboxPopup.titleText = "Send \"" + msg.itemName + "\" to Trash";
                             lightboxPopup.bodyText = "Sending this item to the Trash means you will no longer own this item " +
-                                "and it will be inaccessible to you from Purchases.\n\nThis action cannot be undone.";
+                                "and it will be inaccessible to you from Inventory.\n\nThis action cannot be undone.";
                             lightboxPopup.button1text = "CANCEL";
                             lightboxPopup.button1method = function() {
                                 lightboxPopup.visible = false;
diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp
index a3950c8e96..9496336ae1 100755
--- a/interface/src/avatar/OtherAvatar.cpp
+++ b/interface/src/avatar/OtherAvatar.cpp
@@ -447,6 +447,10 @@ void OtherAvatar::handleChangedAvatarEntityData() {
             EntityItemProperties properties;
             int32_t bytesLeftToRead = data.size();
             unsigned char* dataAt = (unsigned char*)(data.data());
+            // FIXME: This function will cause unintented changes in SpaillyNestable
+            // E.g overriding the ID index of an exisiting entity to temporary entity
+            // in the following map QHash<QUuid, SpatiallyNestableWeakPointer> _children;
+            // Andrew Meadows will address this issue
             if (!properties.constructFromBuffer(dataAt, bytesLeftToRead)) {
                 // properties are corrupt
                 continue;
@@ -489,6 +493,17 @@ void OtherAvatar::handleChangedAvatarEntityData() {
             bool success = true;
             if (entity) {
                 QUuid oldParentID = entity->getParentID();
+
+                // Since  has overwrtiiten the back pointer
+                // from the parent children map (see comment for function call above),
+                // we need to for reset the back pointer in the map correctly by setting the parentID, but
+                // since the parentID of the entity has not changed we first need to set it some ither ID,
+                // then set the the original ID for the changes to take effect
+                // TODO: This is a horrible hack and once properties.constructFromBuffer no longer causes
+                // side effects...remove the following three lines
+                const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}");
+                entity->setParentID(NULL_ID);
+                entity->setParentID(oldParentID);
                 if (entityTree->updateEntity(entityID, properties)) {
                     entity->updateLastEditedFromRemote();
                 } else {
diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp
index f7a7dd861a..4e988334f9 100644
--- a/libraries/animation/src/AnimationCache.cpp
+++ b/libraries/animation/src/AnimationCache.cpp
@@ -36,12 +36,13 @@ AnimationPointer AnimationCache::getAnimation(const QUrl& url) {
     return getResource(url).staticCast<Animation>();
 }
 
-QSharedPointer<Resource> AnimationCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-    const void* extra) {
+QSharedPointer<Resource> AnimationCache::createResource(const QUrl& url) {
     return QSharedPointer<Resource>(new Animation(url), &Resource::deleter);
 }
 
-Animation::Animation(const QUrl& url) : Resource(url) {}
+QSharedPointer<Resource> AnimationCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
+    return QSharedPointer<Resource>(new Animation(*resource.staticCast<Animation>().data()), &Resource::deleter);
+}
 
 AnimationReader::AnimationReader(const QUrl& url, const QByteArray& data) :
     _url(url),
diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h
index 2f8168625e..eea64475df 100644
--- a/libraries/animation/src/AnimationCache.h
+++ b/libraries/animation/src/AnimationCache.h
@@ -34,9 +34,9 @@ public:
     Q_INVOKABLE AnimationPointer getAnimation(const QUrl& url);
 
 protected:
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) override;
+    QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) override;
 
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-        const void* extra) override;
 private:
     explicit AnimationCache(QObject* parent = NULL);
     virtual ~AnimationCache() { }
@@ -62,7 +62,8 @@ class Animation : public Resource {
 
 public:
 
-    explicit Animation(const QUrl& url);
+    Animation(const Animation& other) : Resource(other), _hfmModel(other._hfmModel) {}
+    Animation(const QUrl& url) : Resource(url) {}
 
     QString getType() const override { return "Animation"; }
 
diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp
index 845fd6ab4f..343de46e9a 100644
--- a/libraries/audio/src/SoundCache.cpp
+++ b/libraries/audio/src/SoundCache.cpp
@@ -33,9 +33,12 @@ SharedSoundPointer SoundCache::getSound(const QUrl& url) {
     return getResource(url).staticCast<Sound>();
 }
 
-QSharedPointer<Resource> SoundCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-    const void* extra) {
+QSharedPointer<Resource> SoundCache::createResource(const QUrl& url) {
     auto resource = QSharedPointer<Resource>(new Sound(url), &Resource::deleter);
     resource->setLoadPriority(this, SOUNDS_LOADING_PRIORITY);
     return resource;
 }
+
+QSharedPointer<Resource> SoundCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
+    return QSharedPointer<Resource>(new Sound(*resource.staticCast<Sound>().data()), &Resource::deleter);
+}
\ No newline at end of file
diff --git a/libraries/audio/src/SoundCache.h b/libraries/audio/src/SoundCache.h
index 64d392a41d..48c3354877 100644
--- a/libraries/audio/src/SoundCache.h
+++ b/libraries/audio/src/SoundCache.h
@@ -24,8 +24,9 @@ public:
     Q_INVOKABLE SharedSoundPointer getSound(const QUrl& url);
 
 protected:
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-        const void* extra) override;
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) override;
+    QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) override;
+
 private:
     SoundCache(QObject* parent = NULL);
 };
diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
index fea46d7a6e..ae9fdf572a 100644
--- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
@@ -280,11 +280,7 @@ bool RenderableModelEntityItem::findDetailedParabolaIntersection(const glm::vec3
 }
 
 void RenderableModelEntityItem::fetchCollisionGeometryResource() {
-    QUrl hullURL(getCollisionShapeURL());
-    QUrlQuery queryArgs(hullURL);
-    queryArgs.addQueryItem("collision-hull", "");
-    hullURL.setQuery(queryArgs);
-    _compoundShapeResource = DependencyManager::get<ModelCache>()->getCollisionGeometryResource(hullURL);
+    _compoundShapeResource = DependencyManager::get<ModelCache>()->getCollisionGeometryResource(getCollisionShapeURL());
 }
 
 bool RenderableModelEntityItem::computeShapeFailedToLoad() {
diff --git a/libraries/fbx/src/FBXSerializer_Material.cpp b/libraries/fbx/src/FBXSerializer_Material.cpp
index 0a1d15b72a..9caf713e75 100644
--- a/libraries/fbx/src/FBXSerializer_Material.cpp
+++ b/libraries/fbx/src/FBXSerializer_Material.cpp
@@ -76,13 +76,12 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& ma
 }
 
 void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) {
-
-    QString materialMapString = mapping.value("materialMap").toString();
-    QJsonDocument materialMapDocument = QJsonDocument::fromJson(materialMapString.toUtf8());
-    QJsonObject materialMap = materialMapDocument.object();
-    if (!materialMapString.isEmpty()) {
-        if (materialMapDocument.isEmpty() || materialMap.isEmpty()) {
-            qCDebug(modelformat) << "fbx Material Map found but did not produce valid JSON:" << materialMapString;
+    QJsonObject materialMap;
+    if (mapping.contains("materialMap")) {
+        QByteArray materialMapValue = mapping.value("materialMap").toByteArray();
+        materialMap = QJsonDocument::fromJson(materialMapValue).object();
+        if (materialMap.isEmpty()) {
+            qCDebug(modelformat) << "fbx Material Map found but did not produce valid JSON:" << materialMapValue;
         }
     }
     for (QHash<QString, HFMMaterial>::iterator it = _hfmMaterials.begin(); it != _hfmMaterials.end(); it++) {
diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp
index 99087954eb..41b660f722 100644
--- a/libraries/fbx/src/FSTReader.cpp
+++ b/libraries/fbx/src/FSTReader.cpp
@@ -88,7 +88,7 @@ QByteArray FSTReader::writeMapping(const QVariantHash& mapping) {
     << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD;
     QBuffer buffer;
     buffer.open(QIODevice::WriteOnly);
-    
+
     for (auto key : PREFERED_ORDER) {
         auto it = mapping.find(key);
         if (it != mapping.constEnd()) {
@@ -104,7 +104,7 @@ QByteArray FSTReader::writeMapping(const QVariantHash& mapping) {
             }
         }
     }
-    
+
     for (auto it = mapping.constBegin(); it != mapping.constEnd(); it++) {
         if (!PREFERED_ORDER.contains(it.key())) {
             writeVariant(buffer, it);
diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp
index 51f721e985..d6de3d4b25 100755
--- a/libraries/fbx/src/GLTFSerializer.cpp
+++ b/libraries/fbx/src/GLTFSerializer.cpp
@@ -124,6 +124,31 @@ bool GLTFSerializer::getObjectArrayVal(const QJsonObject& object, const QString&
     return _defined;
 }
 
+QByteArray GLTFSerializer::setGLBChunks(const QByteArray& data) {
+    int byte = 4; 
+    int jsonStart = data.indexOf("JSON", Qt::CaseSensitive);
+    int binStart = data.indexOf("BIN", Qt::CaseSensitive);
+    int jsonLength, binLength;
+    QByteArray jsonLengthChunk, binLengthChunk;
+
+    jsonLengthChunk = data.mid(jsonStart - byte, byte);
+    QDataStream tempJsonLen(jsonLengthChunk);
+    tempJsonLen.setByteOrder(QDataStream::LittleEndian);
+    tempJsonLen >> jsonLength;
+    QByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength);
+
+    if (binStart != -1) {
+        binLengthChunk = data.mid(binStart - byte, byte);
+
+        QDataStream tempBinLen(binLengthChunk);
+        tempBinLen.setByteOrder(QDataStream::LittleEndian);
+        tempBinLen >> binLength;
+
+        _glbBinary = data.mid(binStart + byte, binLength);
+    }
+    return jsonChunk;
+}
+
 int GLTFSerializer::getMeshPrimitiveRenderingMode(const QString& type)
 {
     if (type == "POINTS") {
@@ -309,6 +334,14 @@ bool GLTFSerializer::addBuffer(const QJsonObject& object) {
     GLTFBuffer buffer;
    
     getIntVal(object, "byteLength", buffer.byteLength, buffer.defined);
+
+    if (_url.toString().endsWith("glb")) {
+        if (!_glbBinary.isEmpty()) {
+            buffer.blob = _glbBinary;
+        } else {
+            return false;
+        }
+    }
     if (getStringVal(object, "uri", buffer.uri, buffer.defined)) {
         if (!readBinary(buffer.uri, buffer.blob)) {
             return false;
@@ -535,9 +568,16 @@ bool GLTFSerializer::addTexture(const QJsonObject& object) {
 
 bool GLTFSerializer::parseGLTF(const QByteArray& data) {
     PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr);
-    
-    QJsonDocument d = QJsonDocument::fromJson(data);
+
+    QByteArray jsonChunk = data;
+
+    if (_url.toString().endsWith("glb") && data.indexOf("glTF") == 0 && data.contains("JSON")) {
+        jsonChunk = setGLBChunks(data);
+    }    
+   
+    QJsonDocument d = QJsonDocument::fromJson(jsonChunk);
     QJsonObject jsFile = d.object();
+
     bool success = setAsset(jsFile);
     if (success) {
         QJsonArray accessors;
@@ -924,6 +964,10 @@ MediaType GLTFSerializer::getMediaType() const {
     MediaType mediaType("gltf");
     mediaType.extensions.push_back("gltf");
     mediaType.webMediaTypes.push_back("model/gltf+json");
+
+    mediaType.extensions.push_back("glb");
+    mediaType.webMediaTypes.push_back("model/gltf-binary");
+
     return mediaType;
 }
 
@@ -932,9 +976,9 @@ std::unique_ptr<hfm::Serializer::Factory> GLTFSerializer::getFactory() const {
 }
 
 HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) {
-    
-    _url = url;
 
+    _url = url;
+    
     // Normalize url for local files
     QUrl normalizeUrl = DependencyManager::get<ResourceManager>()->normalizeURL(_url);
     if (normalizeUrl.scheme().isEmpty() || (normalizeUrl.scheme() == "file")) {
@@ -1032,7 +1076,7 @@ QNetworkReply* GLTFSerializer::request(QUrl& url, bool isTest) {
 HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) {
     HFMTexture fbxtex = HFMTexture();
     fbxtex.texcoordSet = 0;
-    
+  
     if (texture.defined["source"]) {
         QString url = _file.images[texture.source].uri;
 
@@ -1041,6 +1085,17 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) {
         qCDebug(modelformat) << "fname: " << fname;
         fbxtex.name = fname;
         fbxtex.filename = textureUrl.toEncoded();
+        
+        if (_url.toString().endsWith("glb") && !_glbBinary.isEmpty()) {
+            int bufferView = _file.images[texture.source].bufferView;
+       
+            GLTFBufferView& imagesBufferview = _file.bufferviews[bufferView];
+            int offset = imagesBufferview.byteOffset;
+            int length = imagesBufferview.byteLength;
+
+            fbxtex.content = _glbBinary.mid(offset, length);
+            fbxtex.filename = textureUrl.toEncoded().append(texture.source);
+        }
 
         if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,")) {
             fbxtex.content = requestEmbeddedData(url); 
diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h
index 57ea126a7b..a361e09fa6 100755
--- a/libraries/fbx/src/GLTFSerializer.h
+++ b/libraries/fbx/src/GLTFSerializer.h
@@ -709,6 +709,7 @@ public:
 private:
     GLTFFile _file;
     QUrl _url;
+    QByteArray _glbBinary;
 
     glm::mat4 getModelTransform(const GLTFNode& node);
 
@@ -731,6 +732,8 @@ private:
                            QVector<double>& values, QMap<QString, bool>&  defined);
     bool getObjectArrayVal(const QJsonObject& object, const QString& fieldname, 
                            QJsonArray& objects, QMap<QString, bool>& defined);
+
+    QByteArray setGLBChunks(const QByteArray& data);
     
     int getMaterialAlphaMode(const QString& type);
     int getAccessorType(const QString& type);
diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp
index e9494a1271..082edd47bc 100644
--- a/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp
+++ b/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp
@@ -60,9 +60,17 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) {
     switch (texture.getType()) {
     case Texture::TEX_2D:
         if (!texture.isArray()) {
-            return GL_TEXTURE_2D;
+            if (!texture.isMultisample()) {
+                return GL_TEXTURE_2D;
+            } else {
+                return GL_TEXTURE_2D_MULTISAMPLE;
+            }
         } else {
-            return GL_TEXTURE_2D_ARRAY;
+            if (!texture.isMultisample()) {
+                return GL_TEXTURE_2D_ARRAY;
+            } else {
+                return GL_TEXTURE_2D_MULTISAMPLE_ARRAY;
+            }
         }
         break;
 
@@ -81,7 +89,9 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) {
 uint8_t GLTexture::getFaceCount(GLenum target) {
     switch (target) {
         case GL_TEXTURE_2D:
+        case GL_TEXTURE_2D_MULTISAMPLE:
         case GL_TEXTURE_2D_ARRAY:
+        case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
             return TEXTURE_2D_NUM_FACES;
         case GL_TEXTURE_CUBE_MAP:
             return TEXTURE_CUBE_NUM_FACES;
@@ -96,15 +106,20 @@ const std::vector<GLenum>& GLTexture::getFaceTargets(GLenum target) {
         GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
         GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
     };
-    static const std::vector<GLenum> faceTargets {
+    static const std::vector<GLenum> face2DTargets {
         GL_TEXTURE_2D
     };
-    static const std::vector<GLenum> arrayFaceTargets{ 
+    static const std::vector<GLenum> face2DMSTargets{
+        GL_TEXTURE_2D_MULTISAMPLE
+    }; 
+    static const std::vector<GLenum> arrayFaceTargets{
         GL_TEXTURE_2D_ARRAY 
     };
     switch (target) {
     case GL_TEXTURE_2D:
-        return faceTargets;
+        return face2DTargets;
+    case GL_TEXTURE_2D_MULTISAMPLE:
+        return face2DMSTargets;
     case GL_TEXTURE_2D_ARRAY:
         return arrayFaceTargets;
     case GL_TEXTURE_CUBE_MAP:
@@ -114,7 +129,7 @@ const std::vector<GLenum>& GLTexture::getFaceTargets(GLenum target) {
         break;
     }
     Q_UNREACHABLE();
-    return faceTargets;
+    return face2DTargets;
 }
 
 GLTexture::GLTexture(const std::weak_ptr<GLBackend>& backend, const Texture& texture, GLuint id) :
diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp
index 1d512103bd..0f50b0724e 100644
--- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp
+++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp
@@ -66,6 +66,8 @@ public:
                     if (gltexture) {
                         if (gltexture->_target == GL_TEXTURE_2D) {
                             glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0);
+                        } else if (gltexture->_target == GL_TEXTURE_2D_MULTISAMPLE) {
+                            glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D_MULTISAMPLE, gltexture->_texture, 0);
                         } else {
                             glFramebufferTextureLayer(GL_FRAMEBUFFER, colorAttachments[unit], gltexture->_texture, 0,
                                                       b._subresource);
@@ -98,6 +100,8 @@ public:
             if (gltexture) {
                 if (gltexture->_target == GL_TEXTURE_2D) {
                     glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0);
+                } else if (gltexture->_target == GL_TEXTURE_2D_MULTISAMPLE) {
+                    glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D_MULTISAMPLE, gltexture->_texture, 0);
                 } else {
                     glFramebufferTextureLayer(GL_FRAMEBUFFER, attachement, gltexture->_texture, 0,
                                               _gpuObject.getDepthStencilBufferSubresource());
diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp
index 4068865274..f47211555a 100644
--- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp
+++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp
@@ -216,19 +216,29 @@ void GL41FixedAllocationTexture::allocateStorage() const {
     const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat());
     const auto numMips = _gpuObject.getNumMips();
     const auto numSlices = _gpuObject.getNumSlices();
+    const auto numSamples = _gpuObject.getNumSamples();
 
     // glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y);
-    for (GLint level = 0; level < numMips; level++) {
-        Vec3u dimensions = _gpuObject.evalMipDimensions(level);
-        for (GLenum target : getFaceTargets(_target)) {
-            if (!_gpuObject.isArray()) {
-                glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format,
-                             texelFormat.type, nullptr);
-            } else {
-                glTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0,
-                             texelFormat.format, texelFormat.type, nullptr);
+    if (!_gpuObject.isMultisample()) {
+        for (GLint level = 0; level < numMips; level++) {
+            Vec3u dimensions = _gpuObject.evalMipDimensions(level);
+            for (GLenum target : getFaceTargets(_target)) {
+                if (!_gpuObject.isArray()) {
+                    glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format,
+                                texelFormat.type, nullptr);
+                } else {
+                    glTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0,
+                                    texelFormat.format, texelFormat.type, nullptr);
+                }
             }
         }
+    } else {
+        const auto dimensions = _gpuObject.getDimensions();
+        if (!_gpuObject.isArray()) {
+            glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, numSamples, texelFormat.internalFormat, dimensions.x, dimensions.y, GL_FALSE);
+        } else {
+            glTexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, numSamples, texelFormat.internalFormat, dimensions.x, dimensions.y, dimensions.z, GL_FALSE);
+        }
     }
 
     glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0);
diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp
index 86332558e3..7a299e792b 100644
--- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp
+++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp
@@ -62,6 +62,8 @@ public:
                     if (gltexture) {
                         if (gltexture->_target == GL_TEXTURE_2D) {
                             glNamedFramebufferTexture(_id, colorAttachments[unit], gltexture->_texture, 0);
+                        } else if (gltexture->_target == GL_TEXTURE_2D_MULTISAMPLE) {
+                            glNamedFramebufferTexture(_id, colorAttachments[unit], gltexture->_texture, 0);
                         } else {
                             glNamedFramebufferTextureLayer(_id, colorAttachments[unit], gltexture->_texture, 0, b._subresource);
                         }
@@ -93,6 +95,9 @@ public:
             if (gltexture) {
                 if (gltexture->_target == GL_TEXTURE_2D) {
                     glNamedFramebufferTexture(_id, attachement, gltexture->_texture, 0);
+                }
+                else if (gltexture->_target == GL_TEXTURE_2D_MULTISAMPLE) {
+                    glNamedFramebufferTexture(_id, attachement, gltexture->_texture, 0);
                 } else {
                     glNamedFramebufferTextureLayer(_id, attachement, gltexture->_texture, 0,
                                                    _gpuObject.getDepthStencilBufferSubresource());
diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp
index a8b5ec85e8..4aff76df21 100644
--- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp
+++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp
@@ -380,11 +380,22 @@ void GL45FixedAllocationTexture::allocateStorage() const {
     const auto dimensions = _gpuObject.getDimensions();
     const auto mips = _gpuObject.getNumMips();
     const auto numSlices = _gpuObject.getNumSlices();
+    const auto numSamples = _gpuObject.getNumSamples();
 
-    if (!_gpuObject.isArray()) {
-        glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y);
+
+    if (!_gpuObject.isMultisample()) {
+        if (!_gpuObject.isArray()) {
+            glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y);
+        } else {
+            glTextureStorage3D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices);
+        }
     } else {
-        glTextureStorage3D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices);
+        if (!_gpuObject.isArray()) {
+            glTextureStorage2DMultisample(_id, numSamples, texelFormat.internalFormat, dimensions.x, dimensions.y, GL_FALSE);
+        }
+        else {
+            glTextureStorage3DMultisample(_id, numSamples, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, GL_FALSE);
+        }
     }
 
     glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0);
diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp
index 90ce8c853a..36b37083cb 100644
--- a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp
+++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp
@@ -130,6 +130,8 @@ public:
 
                             }
 #endif
+                        } else if (gltexture->_target == GL_TEXTURE_2D_MULTISAMPLE) {
+                            glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D_MULTISAMPLE, gltexture->_texture, 0);
                         } else {
                             glFramebufferTextureLayer(GL_FRAMEBUFFER, colorAttachments[unit], gltexture->_texture, 0,
                                                       b._subresource);
@@ -162,6 +164,8 @@ public:
             if (gltexture) {
                 if (gltexture->_target == GL_TEXTURE_2D) {
                     glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0);
+                } else if (gltexture->_target == GL_TEXTURE_2D_MULTISAMPLE) {
+                    glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D_MULTISAMPLE, gltexture->_texture, 0);
                 } else {
                     glFramebufferTextureLayer(GL_FRAMEBUFFER, attachement, gltexture->_texture, 0,
                                               _gpuObject.getDepthStencilBufferSubresource());
diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp
index 23dc271af9..4b2d4d09e6 100644
--- a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp
+++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp
@@ -272,28 +272,40 @@ void GLESFixedAllocationTexture::allocateStorage() const {
     const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat());
     const auto numMips = _gpuObject.getNumMips();
     const auto numSlices = _gpuObject.getNumSlices();
+    const auto numSamples = _gpuObject.getNumSamples();
 
     // glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y);
-    for (GLint level = 0; level < numMips; level++) {
-        Vec3u dimensions = _gpuObject.evalMipDimensions(level);
-        for (GLenum target : getFaceTargets(_target)) {
-            if (texelFormat.isCompressed()) {
-                auto size = getCompressedImageSize(dimensions.x, dimensions.y, texelFormat.internalFormat);
-                if (!_gpuObject.isArray()) {
-                    glCompressedTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, size, nullptr);
+    if (!_gpuObject.isMultisample()) {
+        for (GLint level = 0; level < numMips; level++) {
+            Vec3u dimensions = _gpuObject.evalMipDimensions(level);
+            for (GLenum target : getFaceTargets(_target)) {
+                if (texelFormat.isCompressed()) {
+                    auto size = getCompressedImageSize(dimensions.x, dimensions.y, texelFormat.internalFormat);
+                    if (!_gpuObject.isArray()) {
+                        glCompressedTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, size, nullptr);
+                    } else {
+                        glCompressedTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0, size * numSlices, nullptr);
+                    }
                 } else {
-                    glCompressedTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0, size * numSlices, nullptr);
-                }
-            } else {
-                if (!_gpuObject.isArray()) {
-                    glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format,
-                                texelFormat.type, nullptr);
-                } else {
-                    glTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0,
-                                texelFormat.format, texelFormat.type, nullptr);
+                    if (!_gpuObject.isArray()) {
+                        glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format,
+                                    texelFormat.type, nullptr);
+                    } else {
+                        glTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0,
+                                    texelFormat.format, texelFormat.type, nullptr);
+                    }
                 }
             }
         }
+    } else {
+        const auto dimensions = _gpuObject.getDimensions();
+        if (!_gpuObject.isArray()) {
+            glTexStorage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, numSamples,
+                                      texelFormat.internalFormat, dimensions.x, dimensions.y,
+                                      GL_FALSE);
+        } else {
+            // NOT SUPPORTED (yet)
+        }       
     }
 
     glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0);
diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp
index c6f3cd9b9a..5c2e181810 100755
--- a/libraries/gpu/src/gpu/Texture.cpp
+++ b/libraries/gpu/src/gpu/Texture.cpp
@@ -176,10 +176,18 @@ TexturePointer Texture::createRenderBuffer(const Element& texelFormat, uint16 wi
     return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler);
 }
 
+TexturePointer Texture::createRenderBufferMultisample(const Element& texelFormat, uint16 width, uint16 height, uint16 numSamples, const Sampler& sampler) {
+    return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, numSamples, 0, gpu::Texture::SINGLE_MIP, sampler);
+}
+
 TexturePointer Texture::createRenderBufferArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numMips, const Sampler& sampler) {
     return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, numSlices, numMips, sampler);
 }
 
+TexturePointer Texture::createRenderBufferMultisampleArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numSamples, const Sampler& sampler) {
+    return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, numSamples, numSlices, gpu::Texture::SINGLE_MIP, sampler);
+}
+
 TexturePointer Texture::create1D(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) {
     return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, numMips, sampler);
 }
diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h
index 23bfff6873..d410671481 100755
--- a/libraries/gpu/src/gpu/Texture.h
+++ b/libraries/gpu/src/gpu/Texture.h
@@ -387,7 +387,9 @@ public:
     static TexturePointer create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler());
     static TexturePointer createCube(const Element& texelFormat, uint16 width, uint16 numMips = 1, const Sampler& sampler = Sampler());
     static TexturePointer createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler());
+    static TexturePointer createRenderBufferMultisample(const Element& texelFormat, uint16 width, uint16 height, uint16 numSamples, const Sampler& sampler = Sampler());
     static TexturePointer createRenderBufferArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler());
+    static TexturePointer createRenderBufferMultisampleArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numSamples, const Sampler& sampler = Sampler());
     static TexturePointer createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler());
     static TexturePointer createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler());
 
@@ -435,6 +437,7 @@ public:
     uint16 getNumSamples() const { return _numSamples; }
     // NumSamples can only have certain values based on the hw
     static uint16 evalNumSamplesUsed(uint16 numSamplesTried);
+    bool isMultisample() const { return _numSamples > 1; }
 
     // max mip is in the range [ 0 if no sub mips, log2(max(width, height, depth))]
     // It is defined at creation time (immutable)
diff --git a/libraries/graphics/src/graphics/Material.cpp b/libraries/graphics/src/graphics/Material.cpp
index 7743c4bf50..3ef407b7d9 100755
--- a/libraries/graphics/src/graphics/Material.cpp
+++ b/libraries/graphics/src/graphics/Material.cpp
@@ -105,7 +105,7 @@ void Material::setMetallic(float metallic) {
 
 void Material::setScattering(float scattering) {
     scattering = glm::clamp(scattering, 0.0f, 1.0f);
-    _key.setMetallic(scattering > 0.0f);
+    _key.setScattering(scattering > 0.0f);
     _scattering = scattering;
 }
 
diff --git a/libraries/model-networking/src/model-networking/MaterialCache.cpp b/libraries/model-networking/src/model-networking/MaterialCache.cpp
index b6550a5e9e..aaa9767397 100644
--- a/libraries/model-networking/src/model-networking/MaterialCache.cpp
+++ b/libraries/model-networking/src/model-networking/MaterialCache.cpp
@@ -417,9 +417,13 @@ MaterialCache& MaterialCache::instance() {
 }
 
 NetworkMaterialResourcePointer MaterialCache::getMaterial(const QUrl& url) {
-    return ResourceCache::getResource(url, QUrl(), nullptr).staticCast<NetworkMaterialResource>();
+    return ResourceCache::getResource(url).staticCast<NetworkMaterialResource>();
 }
 
-QSharedPointer<Resource> MaterialCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback, const void* extra) {
+QSharedPointer<Resource> MaterialCache::createResource(const QUrl& url) {
     return QSharedPointer<Resource>(new NetworkMaterialResource(url), &Resource::deleter);
+}
+
+QSharedPointer<Resource> MaterialCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
+    return QSharedPointer<Resource>(new NetworkMaterialResource(*resource.staticCast<NetworkMaterialResource>().data()), &Resource::deleter);
 }
\ No newline at end of file
diff --git a/libraries/model-networking/src/model-networking/MaterialCache.h b/libraries/model-networking/src/model-networking/MaterialCache.h
index 074cd6c98d..6abadfc030 100644
--- a/libraries/model-networking/src/model-networking/MaterialCache.h
+++ b/libraries/model-networking/src/model-networking/MaterialCache.h
@@ -53,7 +53,8 @@ public:
     NetworkMaterialResourcePointer getMaterial(const QUrl& url);
 
 protected:
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback, const void* extra) override;
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) override;
+    QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) override;
 };
 
 #endif
diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp
index 299a2ad729..a4eba0c7a9 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.cpp
+++ b/libraries/model-networking/src/model-networking/ModelCache.cpp
@@ -40,6 +40,50 @@ public:
     bool combineParts;
 };
 
+// From: https://stackoverflow.com/questions/41145012/how-to-hash-qvariant
+class QVariantHasher {
+public:
+    QVariantHasher() : buff(&bb), ds(&buff) {
+        bb.reserve(1000);
+        buff.open(QIODevice::WriteOnly);
+    }
+    uint hash(const QVariant& v) {
+        buff.seek(0);
+        ds << v;
+        return qHashBits(bb.constData(), buff.pos());
+    }
+private:
+    QByteArray bb;
+    QBuffer buff;
+    QDataStream ds;
+};
+
+namespace std {
+    template <>
+    struct hash<QVariantHash> {
+        size_t operator()(const QVariantHash& a) const {
+            QVariantHasher hasher;
+            return hasher.hash(a);
+        }
+    };
+
+    template <>
+    struct hash<QUrl> {
+        size_t operator()(const QUrl& a) const {
+            return qHash(a);
+        }
+    };
+
+    template <>
+    struct hash<GeometryExtra> {
+        size_t operator()(const GeometryExtra& a) const {
+            size_t result = 0;
+            hash_combine(result, a.mapping, a.textureBaseUrl, a.combineParts);
+            return result;
+        }
+    };
+}
+
 QUrl resolveTextureBaseUrl(const QUrl& url, const QUrl& textureBaseUrl) {
     return textureBaseUrl.isValid() ? textureBaseUrl : url;
 }
@@ -107,10 +151,10 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) {
         }
 
         auto modelCache = DependencyManager::get<ModelCache>();
-        GeometryExtra extra{ _mapping, _textureBaseUrl, false };
+        GeometryExtra extra { _mapping, _textureBaseUrl, false };
 
         // Get the raw GeometryResource
-        _geometryResource = modelCache->getResource(url, QUrl(), &extra).staticCast<GeometryResource>();
+        _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash<GeometryExtra>()(extra)).staticCast<GeometryResource>();
         // Avoid caching nested resources - their references will be held by the parent
         _geometryResource->_isCacheable = false;
 
@@ -253,13 +297,19 @@ void GeometryReader::run() {
 class GeometryDefinitionResource : public GeometryResource {
     Q_OBJECT
 public:
-    GeometryDefinitionResource(const ModelLoader& modelLoader, const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl, bool combineParts) :
-        GeometryResource(url, resolveTextureBaseUrl(url, textureBaseUrl)), _modelLoader(modelLoader), _mapping(mapping), _combineParts(combineParts) {}
+    GeometryDefinitionResource(const ModelLoader& modelLoader, const QUrl& url) : GeometryResource(url), _modelLoader(modelLoader) {}
+    GeometryDefinitionResource(const GeometryDefinitionResource& other) :
+        GeometryResource(other),
+        _modelLoader(other._modelLoader),
+        _mapping(other._mapping),
+        _combineParts(other._combineParts) {}
 
     QString getType() const override { return "GeometryDefinition"; }
 
     virtual void downloadFinished(const QByteArray& data) override;
 
+    void setExtra(void* extra) override;
+
 protected:
     Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping);
 
@@ -269,6 +319,13 @@ private:
     bool _combineParts;
 };
 
+void GeometryDefinitionResource::setExtra(void* extra) {
+    const GeometryExtra* geometryExtra = static_cast<const GeometryExtra*>(extra);
+    _mapping = geometryExtra ? geometryExtra->mapping : QVariantHash();
+    _textureBaseUrl = resolveTextureBaseUrl(_url, geometryExtra ? geometryExtra->textureBaseUrl : QUrl());
+    _combineParts = geometryExtra ? geometryExtra->combineParts : true;
+}
+
 void GeometryDefinitionResource::downloadFinished(const QByteArray& data) {
     if (_url != _effectiveBaseURL) {
         _url = _effectiveBaseURL;
@@ -323,27 +380,26 @@ ModelCache::ModelCache() {
     modelFormatRegistry->addFormat(GLTFSerializer());
 }
 
-QSharedPointer<Resource> ModelCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-                                                    const void* extra) {
+QSharedPointer<Resource> ModelCache::createResource(const QUrl& url) {
     Resource* resource = nullptr;
     if (url.path().toLower().endsWith(".fst")) {
         resource = new GeometryMappingResource(url);
     } else {
-        const GeometryExtra* geometryExtra = static_cast<const GeometryExtra*>(extra);
-        auto mapping = geometryExtra ? geometryExtra->mapping : QVariantHash();
-        auto textureBaseUrl = geometryExtra ? geometryExtra->textureBaseUrl : QUrl();
-        bool combineParts = geometryExtra ? geometryExtra->combineParts : true;
-        resource = new GeometryDefinitionResource(_modelLoader, url, mapping, textureBaseUrl, combineParts);
+        resource = new GeometryDefinitionResource(_modelLoader, url);
     }
 
     return QSharedPointer<Resource>(resource, &Resource::deleter);
 }
 
+QSharedPointer<Resource> ModelCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
+    return QSharedPointer<Resource>(new GeometryDefinitionResource(*resource.staticCast<GeometryDefinitionResource>().data()), &Resource::deleter);
+}
+
 GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url,
                                                           const QVariantHash& mapping, const QUrl& textureBaseUrl) {
     bool combineParts = true;
     GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
-    GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra).staticCast<GeometryResource>();
+    GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash<GeometryExtra>()(geometryExtra)).staticCast<GeometryResource>();
     if (resource) {
         if (resource->isLoaded() && resource->shouldSetTextures()) {
             resource->setTextures();
@@ -356,7 +412,7 @@ GeometryResource::Pointer ModelCache::getCollisionGeometryResource(const QUrl& u
                                                                    const QVariantHash& mapping, const QUrl& textureBaseUrl) {
     bool combineParts = false;
     GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
-    GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra).staticCast<GeometryResource>();
+    GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash<GeometryExtra>()(geometryExtra)).staticCast<GeometryResource>();
     if (resource) {
         if (resource->isLoaded() && resource->shouldSetTextures()) {
             resource->setTextures();
diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h
index 1018bdecd5..497cae86a3 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.h
+++ b/libraries/model-networking/src/model-networking/ModelCache.h
@@ -82,8 +82,12 @@ class GeometryResource : public Resource, public Geometry {
 public:
     using Pointer = QSharedPointer<GeometryResource>;
 
-    GeometryResource(const QUrl& url, const QUrl& textureBaseUrl = QUrl()) :
-        Resource(url), _textureBaseUrl(textureBaseUrl) {}
+    GeometryResource(const QUrl& url) : Resource(url) {}
+    GeometryResource(const GeometryResource& other) :
+        Resource(other),
+        Geometry(other),
+        _textureBaseUrl(other._textureBaseUrl),
+        _isCacheable(other._isCacheable) {}
 
     virtual bool areTexturesLoaded() const override { return isLoaded() && Geometry::areTexturesLoaded(); }
 
@@ -153,8 +157,8 @@ public:
 protected:
     friend class GeometryMappingResource;
 
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-                                                    const void* extra) override;
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) override;
+    QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) override;
 
 private:
     ModelCache();
diff --git a/libraries/model-networking/src/model-networking/ShaderCache.cpp b/libraries/model-networking/src/model-networking/ShaderCache.cpp
index bf7ade07f7..8d060c42f2 100644
--- a/libraries/model-networking/src/model-networking/ShaderCache.cpp
+++ b/libraries/model-networking/src/model-networking/ShaderCache.cpp
@@ -21,11 +21,13 @@ ShaderCache& ShaderCache::instance() {
 }
 
 NetworkShaderPointer ShaderCache::getShader(const QUrl& url) {
-    return ResourceCache::getResource(url, QUrl(), nullptr).staticCast<NetworkShader>();
+    return ResourceCache::getResource(url).staticCast<NetworkShader>();
 }
 
-QSharedPointer<Resource> ShaderCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-    const void* extra) {
+QSharedPointer<Resource> ShaderCache::createResource(const QUrl& url) {
     return QSharedPointer<Resource>(new NetworkShader(url), &Resource::deleter);
 }
 
+QSharedPointer<Resource> ShaderCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
+    return QSharedPointer<Resource>(new NetworkShader(*resource.staticCast<NetworkShader>().data()), &Resource::deleter);
+}
diff --git a/libraries/model-networking/src/model-networking/ShaderCache.h b/libraries/model-networking/src/model-networking/ShaderCache.h
index bd78e6e7e3..fe9edd7ddf 100644
--- a/libraries/model-networking/src/model-networking/ShaderCache.h
+++ b/libraries/model-networking/src/model-networking/ShaderCache.h
@@ -14,6 +14,7 @@
 class NetworkShader : public Resource {
 public:
     NetworkShader(const QUrl& url);
+    NetworkShader(const NetworkShader& other) : Resource(other), _source(other._source) {}
 
     QString getType() const override { return "NetworkShader"; }
 
@@ -31,8 +32,8 @@ public:
     NetworkShaderPointer getShader(const QUrl& url);
 
 protected:
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-        const void* extra) override;
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) override;
+    QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) override;
 };
 
 #endif
diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp
index 4c30dc6d93..d4cf7e6ce9 100644
--- a/libraries/model-networking/src/model-networking/TextureCache.cpp
+++ b/libraries/model-networking/src/model-networking/TextureCache.cpp
@@ -194,10 +194,28 @@ public:
     int maxNumPixels;
 };
 
+namespace std {
+    template <>
+    struct hash<QByteArray> {
+        size_t operator()(const QByteArray& a) const {
+            return qHash(a);
+        }
+    };
+
+    template <>
+    struct hash<TextureExtra> {
+        size_t operator()(const TextureExtra& a) const {
+            size_t result = 0;
+            hash_combine(result, (int)a.type, a.content, a.maxNumPixels);
+            return result;
+        }
+    };
+}
+
 ScriptableResource* TextureCache::prefetch(const QUrl& url, int type, int maxNumPixels) {
     auto byteArray = QByteArray();
     TextureExtra extra = { (image::TextureUsage::Type)type, byteArray, maxNumPixels };
-    return ResourceCache::prefetch(url, &extra);
+    return ResourceCache::prefetch(url, &extra, std::hash<TextureExtra>()(extra));
 }
 
 NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels) {
@@ -211,7 +229,7 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs
         modifiedUrl.setQuery(query.toString());
     }
     TextureExtra extra = { type, content, maxNumPixels };
-    return ResourceCache::getResource(modifiedUrl, QUrl(), &extra).staticCast<NetworkTexture>();
+    return ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash<TextureExtra>()(extra)).staticCast<NetworkTexture>();
 }
 
 gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) {
@@ -305,26 +323,36 @@ gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::Te
     return gpu::TexturePointer(loader(std::move(image), path.toStdString(), shouldCompress, target, false));
 }
 
-QSharedPointer<Resource> TextureCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-    const void* extra) {
-    const TextureExtra* textureExtra = static_cast<const TextureExtra*>(extra);
-    auto type = textureExtra ? textureExtra->type : image::TextureUsage::DEFAULT_TEXTURE;
-    auto content = textureExtra ? textureExtra->content : QByteArray();
-    auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
-    NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels);
-    return QSharedPointer<Resource>(texture, &Resource::deleter);
+QSharedPointer<Resource> TextureCache::createResource(const QUrl& url) {
+    return QSharedPointer<Resource>(new NetworkTexture(url), &Resource::deleter);
+}
+
+QSharedPointer<Resource> TextureCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
+    return QSharedPointer<Resource>(new NetworkTexture(*resource.staticCast<NetworkTexture>().data()), &Resource::deleter);
 }
 
 int networkTexturePointerMetaTypeId = qRegisterMetaType<QWeakPointer<NetworkTexture>>();
 
-NetworkTexture::NetworkTexture(const QUrl& url) :
-Resource(url),
-_type(),
-_maxNumPixels(100)
+NetworkTexture::NetworkTexture(const QUrl& url, bool resourceTexture) :
+    Resource(url),
+    _maxNumPixels(100)
+{
+    if (resourceTexture) {
+        _textureSource = std::make_shared<gpu::TextureSource>(url);
+        _loaded = true;
+    }
+}
+
+NetworkTexture::NetworkTexture(const NetworkTexture& other) :
+    Resource(other),
+    _type(other._type),
+    _currentlyLoadingResourceType(other._currentlyLoadingResourceType),
+    _originalWidth(other._originalWidth),
+    _originalHeight(other._originalHeight),
+    _width(other._width),
+    _height(other._height),
+    _maxNumPixels(other._maxNumPixels)
 {
-    _textureSource = std::make_shared<gpu::TextureSource>(url);
-    _lowestRequestedMipLevel = 0;
-    _loaded = true;
 }
 
 static bool isLocalUrl(const QUrl& url) {
@@ -332,15 +360,15 @@ static bool isLocalUrl(const QUrl& url) {
     return (scheme == HIFI_URL_SCHEME_FILE || scheme == URL_SCHEME_QRC || scheme == RESOURCE_SCHEME);
 }
 
-NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels) :
-    Resource(url),
-    _type(type),
-    _maxNumPixels(maxNumPixels)
-{
-    _textureSource = std::make_shared<gpu::TextureSource>(url, (int)type);
+void NetworkTexture::setExtra(void* extra) {
+    const TextureExtra* textureExtra = static_cast<const TextureExtra*>(extra);
+    _type = textureExtra ? textureExtra->type : image::TextureUsage::DEFAULT_TEXTURE;
+    _maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
+
+    _textureSource = std::make_shared<gpu::TextureSource>(_url, (int)_type);
     _lowestRequestedMipLevel = 0;
 
-    auto fileNameLowercase = url.fileName().toLower();
+    auto fileNameLowercase = _url.fileName().toLower();
     if (fileNameLowercase.endsWith(TEXTURE_META_EXTENSION)) {
         _currentlyLoadingResourceType = ResourceType::META;
     } else if (fileNameLowercase.endsWith(".ktx")) {
@@ -351,17 +379,18 @@ NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type,
 
     _shouldFailOnRedirect = _currentlyLoadingResourceType != ResourceType::KTX;
 
-    if (type == image::TextureUsage::CUBE_TEXTURE) {
+    if (_type == image::TextureUsage::CUBE_TEXTURE) {
         setLoadPriority(this, SKYBOX_LOAD_PRIORITY);
     } else if (_currentlyLoadingResourceType == ResourceType::KTX) {
         setLoadPriority(this, HIGH_MIPS_LOAD_PRIORITY);
     }
 
-    if (!url.isValid()) {
+    if (!_url.isValid()) {
         _loaded = true;
     }
 
     // if we have content, load it after we have our self pointer
+    auto content = textureExtra ? textureExtra->content : QByteArray();
     if (!content.isEmpty()) {
         _startedLoading = true;
         QMetaObject::invokeMethod(this, "downloadFinished", Qt::QueuedConnection, Q_ARG(const QByteArray&, content));
@@ -396,7 +425,7 @@ gpu::TexturePointer NetworkTexture::getFallbackTexture() const {
 class ImageReader : public QRunnable {
 public:
     ImageReader(const QWeakPointer<Resource>& resource, const QUrl& url,
-                const QByteArray& data, int maxNumPixels);
+                const QByteArray& data, size_t extraHash, int maxNumPixels);
     void run() override final;
     void read();
 
@@ -406,6 +435,7 @@ private:
     QWeakPointer<Resource> _resource;
     QUrl _url;
     QByteArray _content;
+    size_t _extraHash;
     int _maxNumPixels;
 };
 
@@ -1039,7 +1069,7 @@ void NetworkTexture::loadTextureContent(const QByteArray& content) {
         return;
     }
 
-    QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _maxNumPixels));
+    QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _extraHash, _maxNumPixels));
 }
 
 void NetworkTexture::refresh() {
@@ -1064,10 +1094,11 @@ void NetworkTexture::refresh() {
     Resource::refresh();
 }
 
-ImageReader::ImageReader(const QWeakPointer<Resource>& resource, const QUrl& url, const QByteArray& data, int maxNumPixels) :
+ImageReader::ImageReader(const QWeakPointer<Resource>& resource, const QUrl& url, const QByteArray& data, size_t extraHash, int maxNumPixels) :
     _resource(resource),
     _url(url),
     _content(data),
+    _extraHash(extraHash),
     _maxNumPixels(maxNumPixels)
 {
     DependencyManager::get<StatTracker>()->incrementStat("PendingProcessing");
@@ -1123,11 +1154,12 @@ void ImageReader::read() {
     }
     auto networkTexture = resource.staticCast<NetworkTexture>();
 
-    // Hash the source image to for KTX caching
+    // Hash the source image and extraHash for KTX caching
     std::string hash;
     {
         QCryptographicHash hasher(QCryptographicHash::Md5);
         hasher.addData(_content);
+        hasher.addData(std::to_string(_extraHash).c_str());
         hash = hasher.result().toHex().toStdString();
     }
 
@@ -1216,11 +1248,11 @@ void ImageReader::read() {
                                 Q_ARG(int, texture->getHeight()));
 }
 
-NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) {
+NetworkTexturePointer TextureCache::getResourceTexture(const QUrl& resourceTextureUrl) {
     gpu::TexturePointer texture;
     if (resourceTextureUrl == SPECTATOR_CAMERA_FRAME_URL) {
         if (!_spectatorCameraNetworkTexture) {
-            _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl));
+            _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl, true));
         }
         if (!_spectatorCameraFramebuffer) {
             getSpectatorCameraFramebuffer(); // initialize frame buffer
@@ -1231,7 +1263,7 @@ NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl)
     // FIXME: Generalize this, DRY up this code
     if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) {
         if (!_hmdPreviewNetworkTexture) {
-            _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl));
+            _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl, true));
         }
         if (_hmdPreviewFramebuffer) {
             texture = _hmdPreviewFramebuffer->getRenderBuffer(0);
diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h
index 3933e3ae56..cdedc64ea5 100644
--- a/libraries/model-networking/src/model-networking/TextureCache.h
+++ b/libraries/model-networking/src/model-networking/TextureCache.h
@@ -45,8 +45,8 @@ class NetworkTexture : public Resource, public Texture {
     Q_OBJECT
 
 public:
-    NetworkTexture(const QUrl& url);
-    NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels);
+    NetworkTexture(const QUrl& url, bool resourceTexture = false);
+    NetworkTexture(const NetworkTexture& other);
     ~NetworkTexture() override;
 
     QString getType() const override { return "NetworkTexture"; }
@@ -63,6 +63,8 @@ public:
 
     Q_INVOKABLE void setOriginalDescriptor(ktx::KTXDescriptor* descriptor) { _originalKtxDescriptor.reset(descriptor); }
 
+    void setExtra(void* extra) override;
+
 signals:
     void networkTextureCreated(const QWeakPointer<NetworkTexture>& self);
 
@@ -181,7 +183,7 @@ public:
     gpu::TexturePointer getTextureByHash(const std::string& hash);
     gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture);
 
-    NetworkTexturePointer getResourceTexture(QUrl resourceTextureUrl);
+    NetworkTexturePointer getResourceTexture(const QUrl& resourceTextureUrl);
     const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height);
     const gpu::FramebufferPointer& getSpectatorCameraFramebuffer();
     const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(int width, int height);
@@ -201,8 +203,8 @@ protected:
     // Overload ResourceCache::prefetch to allow specifying texture type for loads
     Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS);
 
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-        const void* extra) override;
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) override;
+    QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) override;
 
 private:
     friend class ImageReader;
diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp
index 20fe05e7b8..cb7b8c7c82 100644
--- a/libraries/networking/src/ResourceCache.cpp
+++ b/libraries/networking/src/ResourceCache.cpp
@@ -158,8 +158,8 @@ void ScriptableResourceCache::updateTotalSize(const qint64& deltaSize) {
     _resourceCache->updateTotalSize(deltaSize);
 }
 
-ScriptableResource* ScriptableResourceCache::prefetch(const QUrl& url, void* extra) {
-    return _resourceCache->prefetch(url, extra);
+ScriptableResource* ScriptableResourceCache::prefetch(const QUrl& url, void* extra, size_t extraHash) {
+    return _resourceCache->prefetch(url, extra, extraHash);
 }
 
 
@@ -211,20 +211,20 @@ void ScriptableResource::disconnectHelper() {
     }
 }
 
-ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra) {
+ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, size_t extraHash) {
     ScriptableResource* result = nullptr;
 
     if (QThread::currentThread() != thread()) {
         // Must be called in thread to ensure getResource returns a valid pointer
         BLOCKING_INVOKE_METHOD(this, "prefetch",
             Q_RETURN_ARG(ScriptableResource*, result),
-            Q_ARG(QUrl, url), Q_ARG(void*, extra));
+            Q_ARG(QUrl, url), Q_ARG(void*, extra), Q_ARG(size_t, extraHash));
         return result;
     }
 
     result = new ScriptableResource(url);
 
-    auto resource = getResource(url, QUrl(), extra);
+    auto resource = getResource(url, QUrl(), extra, extraHash);
     result->_resource = resource;
     result->setObjectName(url.toString());
 
@@ -265,16 +265,17 @@ ResourceCache::~ResourceCache() {
 void ResourceCache::clearATPAssets() {
     {
         QWriteLocker locker(&_resourcesLock);
-        for (auto& url : _resources.keys()) {
+        QList<QUrl> urls = _resources.keys();
+        for (auto& url : urls) {
             // If this is an ATP resource
             if (url.scheme() == URL_SCHEME_ATP) {
-
-                // Remove it from the resource hash
-                auto resource = _resources.take(url);
-                if (auto strongRef = resource.lock()) {
-                    // Make sure the resource won't reinsert itself
-                    strongRef->setCache(nullptr);
-                    _totalResourcesSize -= strongRef->getBytes();
+                auto resourcesWithExtraHash = _resources.take(url);
+                for (auto& resource : resourcesWithExtraHash) {
+                    if (auto strongRef = resource.lock()) {
+                        // Make sure the resource won't reinsert itself
+                        strongRef->setCache(nullptr);
+                        _totalResourcesSize -= strongRef->getBytes();
+                    }
                 }
             }
         }
@@ -297,16 +298,20 @@ void ResourceCache::refreshAll() {
     clearUnusedResources();
     resetUnusedResourceCounter();
 
-    QHash<QUrl, QWeakPointer<Resource>> resources;
+    QHash<QUrl, QHash<size_t, QWeakPointer<Resource>>> allResources;
     {
         QReadLocker locker(&_resourcesLock);
-        resources = _resources;
+        allResources = _resources;
     }
 
     // Refresh all remaining resources in use
-    foreach (QSharedPointer<Resource> resource, resources) {
-        if (resource) {
-            resource->refresh();
+    // FIXME: this will trigger multiple refreshes for the same resource if they have different hashes
+    for (auto& resourcesWithExtraHash : allResources) {
+        for (auto& resourceWeak : resourcesWithExtraHash) {
+            auto resource = resourceWeak.lock();
+            if (resource) {
+                resource->refresh();
+            }
         }
     }
 }
@@ -338,39 +343,54 @@ void ResourceCache::setRequestLimit(uint32_t limit) {
     }
 }
 
-QSharedPointer<Resource> ResourceCache::getResource(const QUrl& url, const QUrl& fallback, void* extra) {
+QSharedPointer<Resource> ResourceCache::getResource(const QUrl& url, const QUrl& fallback, void* extra, size_t extraHash) {
     QSharedPointer<Resource> resource;
     {
         QReadLocker locker(&_resourcesLock);
-        resource = _resources.value(url).lock();
+        auto& resourcesWithExtraHash = _resources[url];
+        auto resourcesWithExtraHashIter = resourcesWithExtraHash.find(extraHash);
+        if (resourcesWithExtraHashIter != resourcesWithExtraHash.end()) {
+            // We've seen this extra info before
+            resource = resourcesWithExtraHashIter.value().lock();
+        } else if (resourcesWithExtraHash.size() > 0.0f) {
+            // We haven't seen this extra info before, but we've already downloaded the resource.  We need a new copy of this object (with any old hash).
+            resource = createResourceCopy(resourcesWithExtraHash.begin().value().lock());
+            resource->setExtra(extra);
+            resource->setExtraHash(extraHash);
+            resource->setSelf(resource);
+            resource->setCache(this);
+            resource->moveToThread(qApp->thread());
+            connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize);
+            resourcesWithExtraHash.insert(extraHash, resource);
+            removeUnusedResource(resource);
+            resource->ensureLoading();
+        }
     }
     if (resource) {
         removeUnusedResource(resource);
     }
 
-    if (!resource && !url.isValid() && !url.isEmpty() && fallback.isValid()) {
-        resource = getResource(fallback, QUrl());
+    if (!resource && (!url.isValid() || url.isEmpty()) && fallback.isValid()) {
+        resource = getResource(fallback, QUrl(), extra, extraHash);
     }
 
     if (!resource) {
-        resource = createResource(
-            url,
-            fallback.isValid() ?  getResource(fallback, QUrl()) : QSharedPointer<Resource>(),
-            extra);
+        resource = createResource(url);
+        resource->setExtra(extra);
+        resource->setExtraHash(extraHash);
         resource->setSelf(resource);
         resource->setCache(this);
         resource->moveToThread(qApp->thread());
         connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize);
         {
             QWriteLocker locker(&_resourcesLock);
-            _resources.insert(url, resource);
+            _resources[url].insert(extraHash, resource);
         }
         removeUnusedResource(resource);
         resource->ensureLoading();
     }
 
-    DependencyManager::get<ResourceRequestObserver>()->update(
-        resource->getURL(), -1, "ResourceCache::getResource");
+    DependencyManager::get<ResourceRequestObserver>()->update(resource->getURL(), -1, "ResourceCache::getResource");
     return resource;
 }
 
@@ -527,6 +547,27 @@ bool ResourceCache::attemptHighestPriorityRequest() {
 
 static int requestID = 0;
 
+Resource::Resource(const Resource& other) :
+    QObject(),
+    _url(other._url),
+    _effectiveBaseURL(other._effectiveBaseURL),
+    _activeUrl(other._activeUrl),
+    _requestByteRange(other._requestByteRange),
+    _shouldFailOnRedirect(other._shouldFailOnRedirect),
+    _startedLoading(other._startedLoading),
+    _failedToLoad(other._failedToLoad),
+    _loaded(other._loaded),
+    _loadPriorities(other._loadPriorities),
+    _bytesReceived(other._bytesReceived),
+    _bytesTotal(other._bytesTotal),
+    _bytes(other._bytes),
+    _requestID(++requestID),
+    _extraHash(other._extraHash) {
+    if (!other._loaded) {
+        _startedLoading = false;
+    }
+}
+
 Resource::Resource(const QUrl& url) :
     _url(url),
     _activeUrl(url),
@@ -678,7 +719,7 @@ void Resource::setSize(const qint64& bytes) {
 
 void Resource::reinsert() {
     QWriteLocker locker(&_cache->_resourcesLock);
-    _cache->_resources.insert(_url, _self);
+    _cache->_resources[_url].insert(_extraHash, _self);
 }
 
 
diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h
index 275684f73e..740bdadc48 100644
--- a/libraries/networking/src/ResourceCache.h
+++ b/libraries/networking/src/ResourceCache.h
@@ -231,16 +231,16 @@ protected slots:
     // Prefetches a resource to be held by the QScriptEngine.
     // Left as a protected member so subclasses can overload prefetch
     // and delegate to it (see TextureCache::prefetch(const QUrl&, int).
-    ScriptableResource* prefetch(const QUrl& url, void* extra);
+    ScriptableResource* prefetch(const QUrl& url, void* extra, size_t extraHash);
 
     // FIXME: The return type is not recognized by JavaScript.
     /// Loads a resource from the specified URL and returns it.
     /// If the caller is on a different thread than the ResourceCache,
     /// returns an empty smart pointer and loads its asynchronously.
     /// \param fallback a fallback URL to load if the desired one is unavailable
-    /// \param extra extra data to pass to the creator, if appropriate
-    QSharedPointer<Resource> getResource(const QUrl& url, const QUrl& fallback = QUrl(),
-        void* extra = NULL);
+    // FIXME: std::numeric_limits<size_t>::max() could be a valid extraHash
+    QSharedPointer<Resource> getResource(const QUrl& url, const QUrl& fallback = QUrl()) { return getResource(url, fallback, nullptr, std::numeric_limits<size_t>::max()); }
+    QSharedPointer<Resource> getResource(const QUrl& url, const QUrl& fallback, void* extra, size_t extraHash);
 
 private slots:
     void clearATPAssets();
@@ -251,11 +251,11 @@ protected:
     // which should be a QScriptEngine with ScriptableResource registered, so that
     // the QScriptEngine will delete the pointer when it is garbage collected.
     // JSDoc is provided on more general function signature.
-    Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr); }
+    Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr, std::numeric_limits<size_t>::max()); }
 
     /// Creates a new resource.
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
-                                                    const void* extra) = 0;
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) = 0;
+    virtual QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) = 0;
 
     void addUnusedResource(const QSharedPointer<Resource>& resource);
     void removeUnusedResource(const QSharedPointer<Resource>& resource);
@@ -278,7 +278,7 @@ private:
     void resetResourceCounters();
 
     // Resources
-    QHash<QUrl, QWeakPointer<Resource>> _resources;
+    QHash<QUrl, QHash<size_t, QWeakPointer<Resource>>> _resources;
     QReadWriteLock _resourcesLock { QReadWriteLock::Recursive };
     int _lastLRUKey = 0;
 
@@ -332,10 +332,10 @@ public:
      * Prefetches a resource.
      * @function ResourceCache.prefetch
      * @param {string} url - URL of the resource to prefetch.
-     * @param {object} [extra=null]
      * @returns {ResourceObject}
      */
-    Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, void* extra = nullptr);
+    Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr, std::numeric_limits<size_t>::max()); }
+    Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, void* extra, size_t extraHash);
 
 signals:
 
@@ -359,7 +359,8 @@ class Resource : public QObject {
     Q_OBJECT
 
 public:
-    
+
+    Resource(const Resource& other);
     Resource(const QUrl& url);
     virtual ~Resource();
 
@@ -415,6 +416,9 @@ public:
     unsigned int getDownloadAttempts() { return _attempts; }
     unsigned int getDownloadAttemptsRemaining() { return _attemptsRemaining; }
 
+    virtual void setExtra(void* extra) {};
+    void setExtraHash(size_t extraHash) { _extraHash = extraHash; }
+
 signals:
     /// Fired when the resource begins downloading.
     void loading();
@@ -469,7 +473,7 @@ protected:
     virtual bool handleFailedRequest(ResourceRequest::Result result);
 
     QUrl _url;
-    QUrl _effectiveBaseURL{ _url };
+    QUrl _effectiveBaseURL { _url };
     QUrl _activeUrl;
     ByteRange _requestByteRange;
     bool _shouldFailOnRedirect { false };
@@ -492,6 +496,8 @@ protected:
     int _requestID;
     ResourceRequest* _request{ nullptr };
 
+    size_t _extraHash;
+
 public slots:
     void handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTotal);
     void handleReplyFinished();
diff --git a/libraries/recording/src/recording/ClipCache.cpp b/libraries/recording/src/recording/ClipCache.cpp
index c63350de7f..c08dd40ad8 100644
--- a/libraries/recording/src/recording/ClipCache.cpp
+++ b/libraries/recording/src/recording/ClipCache.cpp
@@ -48,8 +48,11 @@ NetworkClipLoaderPointer ClipCache::getClipLoader(const QUrl& url) {
     return getResource(url).staticCast<NetworkClipLoader>();
 }
 
-QSharedPointer<Resource> ClipCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback, const void* extra) {
+QSharedPointer<Resource> ClipCache::createResource(const QUrl& url) {
     qCDebug(recordingLog) << "Loading recording at" << url;
     return QSharedPointer<Resource>(new NetworkClipLoader(url), &Resource::deleter);
 }
 
+QSharedPointer<Resource> ClipCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
+    return QSharedPointer<Resource>(new NetworkClipLoader(*resource.staticCast<NetworkClipLoader>().data()), &Resource::deleter);
+}
\ No newline at end of file
diff --git a/libraries/recording/src/recording/ClipCache.h b/libraries/recording/src/recording/ClipCache.h
index 2c3465e725..202cd2f00e 100644
--- a/libraries/recording/src/recording/ClipCache.h
+++ b/libraries/recording/src/recording/ClipCache.h
@@ -33,6 +33,8 @@ class NetworkClipLoader : public Resource {
     Q_OBJECT
 public:
     NetworkClipLoader(const QUrl& url);
+    NetworkClipLoader(const NetworkClipLoader& other) : Resource(other), _clip(other._clip) {}
+
     virtual void downloadFinished(const QByteArray& data) override;
     ClipPointer getClip() { return _clip; }
     bool completed() { return _failedToLoad || isLoaded(); }
@@ -54,7 +56,8 @@ public slots:
     NetworkClipLoaderPointer getClipLoader(const QUrl& url);
 
 protected:
-    virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback, const void* extra) override;
+    virtual QSharedPointer<Resource> createResource(const QUrl& url) override;
+    QSharedPointer<Resource> createResourceCopy(const QSharedPointer<Resource>& resource) override;
 
 private:
     ClipCache(QObject* parent = nullptr);
diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp
index 85bdf0fadc..5f3763ac2a 100644
--- a/libraries/render-utils/src/RenderPipelines.cpp
+++ b/libraries/render-utils/src/RenderPipelines.cpp
@@ -579,7 +579,7 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
                         } else {
                             forceDefault = true;
                         }
-                        schemaKey.setScattering(true);
+                        schemaKey.setScatteringMap(true);
                     }
                     break;
                 case graphics::MaterialKey::EMISSIVE_MAP_BIT:
diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp
index d192266d7e..64a2adb5d4 100644
--- a/libraries/render-utils/src/ToneMappingEffect.cpp
+++ b/libraries/render-utils/src/ToneMappingEffect.cpp
@@ -79,8 +79,8 @@ void ToneMappingEffect::render(RenderArgs* args, const gpu::TexturePointer& ligh
 
 
 void ToneMappingDeferred::configure(const Config& config) {
-     _toneMappingEffect.setExposure(config.exposure);
-     _toneMappingEffect.setToneCurve((ToneMappingEffect::ToneCurve)config.curve);
+    _toneMappingEffect.setExposure(config.exposure);
+    _toneMappingEffect.setToneCurve((ToneMappingEffect::ToneCurve)config.curve);
 }
 
 void ToneMappingDeferred::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) {
diff --git a/libraries/render-utils/src/toneMapping.slf b/libraries/render-utils/src/toneMapping.slf
index 29f618c2f0..3fe53d9be1 100644
--- a/libraries/render-utils/src/toneMapping.slf
+++ b/libraries/render-utils/src/toneMapping.slf
@@ -37,7 +37,7 @@ int getToneCurve() {
 }
 
 LAYOUT(binding=RENDER_UTILS_TEXTURE_TM_COLOR) uniform sampler2D colorMap;
-        
+
 layout(location=0) in vec2 varTexCoord0;
 layout(location=0) out vec4 outFragColor;
         
diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml
index aef533b7a3..6d98e96780 100644
--- a/scripts/developer/utilities/render/deferredLighting.qml
+++ b/scripts/developer/utilities/render/deferredLighting.qml
@@ -123,7 +123,6 @@ Rectangle {
                         anchors.right: parent.right 
                 }
             }
-
             Item {
                 height: childrenRect.height
                 anchors.left: parent.left
@@ -134,18 +133,17 @@ Rectangle {
                     anchors.left: parent.left           
                 }
 
-                HifiControls.ComboBox {
+                ComboBox {
                     anchors.right: parent.right           
                     currentIndex: 1
-                    model: ListModel {
-                        id: cbItems
-                        ListElement { text: "RGB"; color: "Yellow" }
-                        ListElement { text: "SRGB"; color: "Green" }
-                        ListElement { text: "Reinhard"; color: "Yellow" }
-                        ListElement { text: "Filmic"; color: "White" }
-                    }
+                    model: [
+                        "RGB",
+                        "SRGB",
+                        "Reinhard",
+                        "Filmic",
+                    ]
                     width: 200
-                    onCurrentIndexChanged: { render.mainViewTask.getConfig("ToneMapping")["curve"] = currentIndex }
+                    onCurrentIndexChanged: { render.mainViewTask.getConfig("ToneMapping")["curve"] = currentIndex; }
                 }
             }
         }
@@ -170,7 +168,7 @@ Rectangle {
                 framebuffer.config.mode = mode;
             }
 
-            HifiControls.ComboBox {
+            ComboBox {
                 anchors.right: parent.right           
                 currentIndex: 0
                 model: ListModel {
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js
index ee95312fa4..14a0031f1f 100644
--- a/scripts/system/html/js/entityProperties.js
+++ b/scripts/system/html/js/entityProperties.js
@@ -2328,7 +2328,7 @@ function createTextureProperty(property, elProperty) {
     elInput.setAttribute("id", elementID);
     elInput.setAttribute("type", "text"); 
     
-    let imageLoad = _.debounce(function (url) {
+    let imageLoad = function(url) {
         if (url.slice(0, 5).toLowerCase() === "atp:/") {
             elImage.src = "";
             elImage.style.display = "none";
@@ -2348,15 +2348,12 @@ function createTextureProperty(property, elProperty) {
             elDiv.classList.remove("no-preview");
             elDiv.classList.add("no-texture");
         }
-    }, IMAGE_DEBOUNCE_TIMEOUT);
-    elInput.imageLoad = imageLoad;
-    elInput.oninput = function (event) {
-        // Add throttle
-        let url = event.target.value;
-        imageLoad(url);
-        updateProperty(property.name, url, property.isParticleProperty)
     };
-    elInput.onchange = elInput.oninput;
+    elInput.imageLoad = imageLoad;
+    elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property));
+    elInput.addEventListener('change', function(ev) {
+        imageLoad(ev.target.value);
+    });
 
     elProperty.appendChild(elInput);
     elProperty.appendChild(elDiv);