diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index 614f7bd9fe..bad60643ec 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -663,14 +663,6 @@ void MyAvatar::restoreRoleAnimation(const QString& role) {
     _rig->restoreRoleAnimation(role);
 }
 
-void MyAvatar::prefetchAnimation(const QString& url) {
-    if (QThread::currentThread() != thread()) {
-        QMetaObject::invokeMethod(this, "prefetchAnimation", Q_ARG(const QString&, url));
-        return;
-    }
-    _rig->prefetchAnimation(url);
-}
-
 void MyAvatar::saveData() {
     Settings settings;
     settings.beginGroup("Avatar");
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index e320c0e3de..fee1a9add3 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -143,9 +143,6 @@ public:
     // remove an animation role override and return to the standard animation.
     Q_INVOKABLE void restoreRoleAnimation(const QString& role);
 
-    // prefetch animation
-    Q_INVOKABLE void prefetchAnimation(const QString& url);
-
     // Adds handler(animStateDictionaryIn) => animStateDictionaryOut, which will be invoked just before each animGraph state update.
     // The handler will be called with an animStateDictionaryIn that has all those properties specified by the (possibly empty)
     // propertiesList argument. However for debugging, if the properties argument is null, all internal animGraph state is provided.
diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp
index b3f5e30d40..a7115199a2 100644
--- a/libraries/animation/src/Rig.cpp
+++ b/libraries/animation/src/Rig.cpp
@@ -152,14 +152,6 @@ void Rig::restoreRoleAnimation(const QString& role) {
     }
 }
 
-void Rig::prefetchAnimation(const QString& url) {
-
-    // This will begin loading the NetworkGeometry for the given URL.
-    // which should speed us up if we request it later via overrideAnimation.
-    auto clipNode = std::make_shared<AnimClip>("prefetch", url, 0, 0, 1.0, false, false);
-    _prefetchedAnimations.push_back(clipNode);
-}
-
 void Rig::destroyAnimGraph() {
     _animSkeleton.reset();
     _animLoader.reset();
diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h
index 0ce6f6639e..363006d48c 100644
--- a/libraries/animation/src/Rig.h
+++ b/libraries/animation/src/Rig.h
@@ -94,7 +94,6 @@ public:
     QStringList getAnimationRoles() const;
     void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame);
     void restoreRoleAnimation(const QString& role);
-    void prefetchAnimation(const QString& url);
 
     void initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset);
     void reset(const FBXGeometry& geometry);
@@ -319,7 +318,6 @@ protected:
     SimpleMovingAverage _averageLateralSpeed { 10 };
 
     std::map<QString, AnimNode::Pointer> _origRoleAnimations;
-    std::vector<AnimNode::Pointer> _prefetchedAnimations;
 
     bool _lastEnableInverseKinematics { true };
     bool _enableInverseKinematics { true };
diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
index 8e46521bfc..013385a169 100644
--- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp
+++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
@@ -380,7 +380,7 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
         _pendingAmbientTexture = false;
         _ambientTexture.clear();
     } else {
-        _ambientTexture = textureCache->getTexture(zone->getKeyLightProperties().getAmbientURL(), CUBE_TEXTURE);
+        _ambientTexture = textureCache->getTexture(zone->getKeyLightProperties().getAmbientURL(), NetworkTexture::CUBE_TEXTURE);
         _pendingAmbientTexture = true;
 
         if (_ambientTexture && _ambientTexture->isLoaded()) {
@@ -410,7 +410,7 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
                 _skyboxTexture.clear();
             } else {
                 // Update the Texture of the Skybox with the one pointed by this zone
-                _skyboxTexture = textureCache->getTexture(zone->getSkyboxProperties().getURL(), CUBE_TEXTURE);
+                _skyboxTexture = textureCache->getTexture(zone->getSkyboxProperties().getURL(), NetworkTexture::CUBE_TEXTURE);
                 _pendingSkyboxTexture = true;
 
                 if (_skyboxTexture && _skyboxTexture->isLoaded()) {
diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp
index 6dd1d97d7f..e4fb5c97f8 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.cpp
+++ b/libraries/model-networking/src/model-networking/ModelCache.cpp
@@ -237,13 +237,14 @@ ModelCache::ModelCache() {
 
 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);
+        const GeometryExtra* geometryExtra = static_cast<const GeometryExtra*>(extra);
+        auto mapping = geometryExtra ? geometryExtra->mapping : QVariantHash();
+        auto textureBaseUrl = geometryExtra ? geometryExtra->textureBaseUrl : QUrl();
+        resource = new GeometryDefinitionResource(url, mapping, textureBaseUrl);
     }
 
     return QSharedPointer<Resource>(resource, &Resource::deleter);
@@ -424,7 +425,7 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur
 {
     _textures = Textures(MapChannel::NUM_MAP_CHANNELS);
     if (!material.albedoTexture.filename.isEmpty()) {
-        auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP);
+        auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, NetworkTexture::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP);
         _albedoTransform = material.albedoTexture.transform;
         map->setTextureTransform(_albedoTransform);
 
@@ -441,39 +442,39 @@ NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textur
 
 
     if (!material.normalTexture.filename.isEmpty()) {
-        auto type = (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE);
+        auto type = (material.normalTexture.isBumpmap ? NetworkTexture::BUMP_TEXTURE : NetworkTexture::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);
+        auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, NetworkTexture::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);
+        auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, NetworkTexture::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);
+        auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, NetworkTexture::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);
+        auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, NetworkTexture::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);
+        auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, NetworkTexture::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);
+        auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, NetworkTexture::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);
+        auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP);
         _lightmapTransform = material.lightmapTexture.transform;
         _lightmapParams = material.lightmapParams;
         map->setTextureTransform(_lightmapTransform);
@@ -495,7 +496,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) {
 
     if (!albedoName.isEmpty()) {
         auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl();
-        auto map = fetchTextureMap(url, ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP);
+        auto map = fetchTextureMap(url, NetworkTexture::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP);
         map->setTextureTransform(_albedoTransform);
         // when reassigning the albedo texture we also check for the alpha channel used as opacity
         map->setUseAlphaChannel(true);
@@ -504,39 +505,39 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) {
 
     if (!normalName.isEmpty()) {
         auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl();
-        auto map = fetchTextureMap(url, NORMAL_TEXTURE, MapChannel::NORMAL_MAP);
+        auto map = fetchTextureMap(url, NetworkTexture::NORMAL_TEXTURE, MapChannel::NORMAL_MAP);
         setTextureMap(MapChannel::NORMAL_MAP, map);
     }
 
     if (!roughnessName.isEmpty()) {
         auto url = textureMap.contains(roughnessName) ? textureMap[roughnessName].toUrl() : QUrl();
         // FIXME: If passing a gloss map instead of a roughmap how do we know?
-        auto map = fetchTextureMap(url, ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP);
+        auto map = fetchTextureMap(url, NetworkTexture::ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP);
         setTextureMap(MapChannel::ROUGHNESS_MAP, map);
     }
 
     if (!metallicName.isEmpty()) {
         auto url = textureMap.contains(metallicName) ? textureMap[metallicName].toUrl() : QUrl();
         // FIXME: If passing a specular map instead of a metallic how do we know?
-        auto map = fetchTextureMap(url, METALLIC_TEXTURE, MapChannel::METALLIC_MAP);
+        auto map = fetchTextureMap(url, NetworkTexture::METALLIC_TEXTURE, MapChannel::METALLIC_MAP);
         setTextureMap(MapChannel::METALLIC_MAP, map);
     }
 
     if (!occlusionName.isEmpty()) {
         auto url = textureMap.contains(occlusionName) ? textureMap[occlusionName].toUrl() : QUrl();
-        auto map = fetchTextureMap(url, OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP);
+        auto map = fetchTextureMap(url, NetworkTexture::OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP);
         setTextureMap(MapChannel::OCCLUSION_MAP, map);
     }
 
     if (!emissiveName.isEmpty()) {
         auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl();
-        auto map = fetchTextureMap(url, EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP);
+        auto map = fetchTextureMap(url, NetworkTexture::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP);
         setTextureMap(MapChannel::EMISSIVE_MAP, map);
     }
 
     if (!lightmapName.isEmpty()) {
         auto url = textureMap.contains(lightmapName) ? textureMap[lightmapName].toUrl() : QUrl();
-        auto map = fetchTextureMap(url, LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP);
+        auto map = fetchTextureMap(url, NetworkTexture::LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP);
         map->setTextureTransform(_lightmapTransform);
         map->setLightmapOffsetScale(_lightmapParams.x, _lightmapParams.y);
         setTextureMap(MapChannel::LIGHTMAP_MAP, map);
diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h
index 88e89a57a9..bf47f293e8 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.h
+++ b/libraries/model-networking/src/model-networking/ModelCache.h
@@ -170,6 +170,8 @@ protected:
     const bool& isOriginal() const { return _isOriginal; }
 
 private:
+    using TextureType = NetworkTexture::Type;
+
     // Helpers for the ctors
     QUrl getTextureUrl(const QUrl& baseUrl, const FBXTexture& fbxTexture);
     model::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture,
diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp
index 7d18151f2c..2aaddace88 100644
--- a/libraries/model-networking/src/model-networking/TextureCache.cpp
+++ b/libraries/model-networking/src/model-networking/TextureCache.cpp
@@ -35,6 +35,16 @@ TextureCache::TextureCache() {
     const qint64 TEXTURE_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE;
     setUnusedResourceCacheSize(TEXTURE_DEFAULT_UNUSED_MAX_SIZE);
     setObjectName("TextureCache");
+
+    // Expose enum Type to JS/QML via properties
+    // Despite being one-off, this should be fine, because TextureCache is a SINGLETON_DEPENDENCY
+    QObject* type = new QObject(this);
+    type->setObjectName("TextureType");
+    setProperty("Type", QVariant::fromValue(type));
+    auto metaEnum = QMetaEnum::fromType<Type>();
+    for (int i = 0; i < metaEnum.keyCount(); ++i) {
+        type->setProperty(metaEnum.key(i), metaEnum.value(i));
+    }
 }
 
 TextureCache::~TextureCache() {
@@ -145,60 +155,68 @@ const gpu::TexturePointer& TextureCache::getNormalFittingTexture() {
 /// Extra data for creating textures.
 class TextureExtra {
 public:
-    TextureType type;
+    NetworkTexture::Type type;
     const QByteArray& content;
 };
 
-NetworkTexturePointer TextureCache::getTexture(const QUrl& url, TextureType type, const QByteArray& content) {
+ScriptableResource* TextureCache::prefetch(const QUrl& url, int type) {
+    auto byteArray = QByteArray();
+    TextureExtra extra = { (Type)type, byteArray };
+    return ResourceCache::prefetch(url, &extra);
+}
+
+NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const QByteArray& content) {
     TextureExtra extra = { type, content };
     return ResourceCache::getResource(url, QUrl(), content.isEmpty(), &extra).staticCast<NetworkTexture>();
 }
 
 
-TextureCache::TextureLoaderFunc getTextureLoaderForType(TextureType type) {
+NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type) {
+    using Type = NetworkTexture;
+
     switch (type) {
-        case ALBEDO_TEXTURE: {
+        case Type::ALBEDO_TEXTURE: {
             return model::TextureUsage::createAlbedoTextureFromImage;
             break;
         }
-        case EMISSIVE_TEXTURE: {
+        case Type::EMISSIVE_TEXTURE: {
             return model::TextureUsage::createEmissiveTextureFromImage;
             break;
         }
-        case LIGHTMAP_TEXTURE: {
+        case Type::LIGHTMAP_TEXTURE: {
             return model::TextureUsage::createLightmapTextureFromImage;
             break;
         }
-        case CUBE_TEXTURE: {
+        case Type::CUBE_TEXTURE: {
             return model::TextureUsage::createCubeTextureFromImage;
             break;
         }
-        case BUMP_TEXTURE: {
+        case Type::BUMP_TEXTURE: {
             return model::TextureUsage::createNormalTextureFromBumpImage;
             break;
         }
-        case NORMAL_TEXTURE: {
+        case Type::NORMAL_TEXTURE: {
             return model::TextureUsage::createNormalTextureFromNormalImage;
             break;
         }
-        case ROUGHNESS_TEXTURE: {
+        case Type::ROUGHNESS_TEXTURE: {
             return model::TextureUsage::createRoughnessTextureFromImage;
             break;
         }
-        case GLOSS_TEXTURE: {
+        case Type::GLOSS_TEXTURE: {
             return model::TextureUsage::createRoughnessTextureFromGlossImage;
             break;
         }
-        case SPECULAR_TEXTURE: {
+        case Type::SPECULAR_TEXTURE: {
             return model::TextureUsage::createMetallicTextureFromImage;
             break;
         }
-        case CUSTOM_TEXTURE: {
+        case Type::CUSTOM_TEXTURE: {
             Q_ASSERT(false);
-            return TextureCache::TextureLoaderFunc();
+            return NetworkTexture::TextureLoaderFunc();
             break;
         }
-        case DEFAULT_TEXTURE:
+        case Type::DEFAULT_TEXTURE:
         default: {
             return model::TextureUsage::create2DTextureFromImage;
             break;
@@ -207,7 +225,7 @@ TextureCache::TextureLoaderFunc getTextureLoaderForType(TextureType type) {
 }
 
 /// Returns a texture version of an image file
-gpu::TexturePointer TextureCache::getImageTexture(const QString& path, TextureType type) {
+gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type) {
     QImage image = QImage(path);
     auto loader = getTextureLoaderForType(type);
     return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString()));
@@ -216,11 +234,13 @@ gpu::TexturePointer TextureCache::getImageTexture(const QString& path, TextureTy
 QSharedPointer<Resource> TextureCache::createResource(const QUrl& url,
         const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) {
     const TextureExtra* textureExtra = static_cast<const TextureExtra*>(extra);
-    return QSharedPointer<Resource>(new NetworkTexture(url, textureExtra->type, textureExtra->content),
+    auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE;
+    auto content = textureExtra ? textureExtra->content : QByteArray();
+    return QSharedPointer<Resource>(new NetworkTexture(url, type, content),
         &Resource::deleter);
 }
 
-NetworkTexture::NetworkTexture(const QUrl& url, TextureType type, const QByteArray& content) :
+NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content) :
     Resource(url, !content.isEmpty()),
     _type(type)
 {
diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h
index c614a7ceb3..8fd0b12369 100644
--- a/libraries/model-networking/src/model-networking/TextureCache.h
+++ b/libraries/model-networking/src/model-networking/TextureCache.h
@@ -17,6 +17,7 @@
 #include <QImage>
 #include <QMap>
 #include <QColor>
+#include <QMetaEnum>
 
 #include <DependencyManager.h>
 #include <ResourceCache.h>
@@ -25,79 +26,6 @@
 namespace gpu {
 class Batch;
 }
-class NetworkTexture;
-
-typedef QSharedPointer<NetworkTexture> NetworkTexturePointer;
-
-enum TextureType {
-    DEFAULT_TEXTURE,
-    ALBEDO_TEXTURE,
-    NORMAL_TEXTURE,
-    BUMP_TEXTURE,
-    SPECULAR_TEXTURE,
-    METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey
-    ROUGHNESS_TEXTURE,
-    GLOSS_TEXTURE,
-    EMISSIVE_TEXTURE,
-    CUBE_TEXTURE,
-    OCCLUSION_TEXTURE,
-    LIGHTMAP_TEXTURE,
-    CUSTOM_TEXTURE
-};
-
-/// Stores cached textures, including render-to-texture targets.
-class TextureCache : public ResourceCache, public Dependency {
-    Q_OBJECT
-    SINGLETON_DEPENDENCY
-    
-public:
-    /// Returns the ID of the permutation/normal texture used for Perlin noise shader programs.  This texture
-    /// has two lines: the first, a set of random numbers in [0, 255] to be used as permutation offsets, and
-    /// the second, a set of random unit vectors to be used as noise gradients.
-    const gpu::TexturePointer& getPermutationNormalTexture();
-
-    /// Returns an opaque white texture (useful for a default).
-    const gpu::TexturePointer& getWhiteTexture();
-
-    /// Returns an opaque gray texture (useful for a default).
-    const gpu::TexturePointer& getGrayTexture();
-
-    /// Returns the a pale blue texture (useful for a normal map).
-    const gpu::TexturePointer& getBlueTexture();
-
-    /// Returns the a black texture (useful for a default).
-    const gpu::TexturePointer& getBlackTexture();
-
-    // Returns a map used to compress the normals through a fitting scale algorithm
-    const gpu::TexturePointer& getNormalFittingTexture();
-
-    /// Returns a texture version of an image file
-    static gpu::TexturePointer getImageTexture(const QString& path, TextureType type = DEFAULT_TEXTURE);
-
-    /// Loads a texture from the specified URL.
-    NetworkTexturePointer getTexture(const QUrl& url, TextureType type = DEFAULT_TEXTURE,
-        const QByteArray& content = QByteArray());
-    
-    typedef gpu::Texture* TextureLoader(const QImage& image, const std::string& srcImageName);
-    
-    typedef std::function<TextureLoader> TextureLoaderFunc;
-protected:
-
-    virtual QSharedPointer<Resource> createResource(const QUrl& url,
-        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra);
-        
-private:
-    TextureCache();
-    virtual ~TextureCache();
-    friend class DilatableNetworkTexture;
- 
-    gpu::TexturePointer _permutationNormalTexture;
-    gpu::TexturePointer _whiteTexture;
-    gpu::TexturePointer _grayTexture;
-    gpu::TexturePointer _blueTexture;
-    gpu::TexturePointer _blackTexture;
-    gpu::TexturePointer _normalFittingTexture;
-};
 
 /// A simple object wrapper for an OpenGL texture.
 class Texture {
@@ -107,15 +35,31 @@ public:
 };
 
 /// A texture loaded from the network.
-
 class NetworkTexture : public Resource, public Texture {
     Q_OBJECT
 
 public:
-    
-    typedef TextureCache::TextureLoaderFunc TextureLoaderFunc;
-    
-    NetworkTexture(const QUrl& url, TextureType type, const QByteArray& content);
+     enum Type {
+        DEFAULT_TEXTURE,
+        ALBEDO_TEXTURE,
+        NORMAL_TEXTURE,
+        BUMP_TEXTURE,
+        SPECULAR_TEXTURE,
+        METALLIC_TEXTURE = SPECULAR_TEXTURE, // for now spec and metallic texture are the same, converted to grey
+        ROUGHNESS_TEXTURE,
+        GLOSS_TEXTURE,
+        EMISSIVE_TEXTURE,
+        CUBE_TEXTURE,
+        OCCLUSION_TEXTURE,
+        LIGHTMAP_TEXTURE,
+        CUSTOM_TEXTURE
+    };
+    Q_ENUM(Type)
+
+    typedef gpu::Texture* TextureLoader(const QImage& image, const std::string& srcImageName);
+    using TextureLoaderFunc = std::function<TextureLoader>;
+
+    NetworkTexture(const QUrl& url, Type type, const QByteArray& content);
     NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content);
 
     int getOriginalWidth() const { return _originalWidth; }
@@ -138,12 +82,69 @@ protected:
     Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight);
 
 private:
-    TextureType _type;
-    TextureLoaderFunc _textureLoader;
+    Type _type;
+    TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } };
     int _originalWidth { 0 };
     int _originalHeight { 0 };
     int _width { 0 };
     int _height { 0 };
 };
 
+using NetworkTexturePointer = QSharedPointer<NetworkTexture>;
+
+/// Stores cached textures, including render-to-texture targets.
+class TextureCache : public ResourceCache, public Dependency {
+    Q_OBJECT
+    SINGLETON_DEPENDENCY
+
+    using Type = NetworkTexture::Type;
+
+public:
+    /// Returns the ID of the permutation/normal texture used for Perlin noise shader programs.  This texture
+    /// has two lines: the first, a set of random numbers in [0, 255] to be used as permutation offsets, and
+    /// the second, a set of random unit vectors to be used as noise gradients.
+    const gpu::TexturePointer& getPermutationNormalTexture();
+
+    /// Returns an opaque white texture (useful for a default).
+    const gpu::TexturePointer& getWhiteTexture();
+
+    /// Returns an opaque gray texture (useful for a default).
+    const gpu::TexturePointer& getGrayTexture();
+
+    /// Returns the a pale blue texture (useful for a normal map).
+    const gpu::TexturePointer& getBlueTexture();
+
+    /// Returns the a black texture (useful for a default).
+    const gpu::TexturePointer& getBlackTexture();
+
+    // Returns a map used to compress the normals through a fitting scale algorithm
+    const gpu::TexturePointer& getNormalFittingTexture();
+
+    /// Returns a texture version of an image file
+    static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE);
+
+    /// Loads a texture from the specified URL.
+    NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE,
+        const QByteArray& content = QByteArray());
+    
+protected:
+    // Overload ResourceCache::prefetch to allow specifying texture type for loads
+    Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type);
+
+    virtual QSharedPointer<Resource> createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra);
+        
+private:
+    TextureCache();
+    virtual ~TextureCache();
+    friend class DilatableNetworkTexture;
+ 
+    gpu::TexturePointer _permutationNormalTexture;
+    gpu::TexturePointer _whiteTexture;
+    gpu::TexturePointer _grayTexture;
+    gpu::TexturePointer _blueTexture;
+    gpu::TexturePointer _blackTexture;
+    gpu::TexturePointer _normalFittingTexture;
+};
+
 #endif // hifi_TextureCache_h
diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp
index c0382a5748..4cc8b1d4f0 100644
--- a/libraries/networking/src/ResourceCache.cpp
+++ b/libraries/networking/src/ResourceCache.cpp
@@ -119,6 +119,92 @@ QSharedPointer<Resource> ResourceCacheSharedItems::getHighestPendingRequest() {
     return highestResource;
 }
 
+ScriptableResource::ScriptableResource(const QUrl& url) :
+    QObject(nullptr),
+    _url(url) { }
+
+void ScriptableResource::release() {
+    disconnectHelper();
+    _resource.reset();
+}
+
+bool ScriptableResource::isInScript() const {
+    return _resource && _resource->isInScript();
+}
+
+void ScriptableResource::setInScript(bool isInScript) {
+    if (_resource) {
+        _resource->setInScript(isInScript);
+    }
+}
+
+void ScriptableResource::loadingChanged() {
+    emit stateChanged(LOADING);
+}
+
+void ScriptableResource::loadedChanged() {
+    emit stateChanged(LOADED);
+}
+
+void ScriptableResource::finished(bool success) {
+    disconnectHelper();
+
+    emit stateChanged(success ? FINISHED : FAILED);
+}
+
+void ScriptableResource::disconnectHelper() {
+    if (_progressConnection) {
+        disconnect(_progressConnection);
+    }
+    if (_loadingConnection) {
+        disconnect(_loadingConnection);
+    }
+    if (_loadedConnection) {
+        disconnect(_loadedConnection);
+    }
+    if (_finishedConnection) {
+        disconnect(_finishedConnection);
+    }
+}
+
+ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra) {
+    ScriptableResource* result = nullptr;
+
+    if (QThread::currentThread() != thread()) {
+        // Must be called in thread to ensure getResource returns a valid pointer
+        QMetaObject::invokeMethod(this, "prefetch", Qt::BlockingQueuedConnection,
+            Q_RETURN_ARG(ScriptableResource*, result),
+            Q_ARG(QUrl, url), Q_ARG(void*, extra));
+        return result;
+    }
+
+    result = new ScriptableResource(url);
+
+    auto resource = getResource(url, QUrl(), false, extra);
+    result->_resource = resource;
+    result->setObjectName(url.toString());
+
+    result->_resource = resource;
+    if (resource->isLoaded()) {
+        result->finished(!resource->_failedToLoad);
+    } else {
+        result->_progressConnection = connect(
+            resource.data(), &Resource::onProgress,
+            result, &ScriptableResource::progressChanged);
+        result->_loadingConnection = connect(
+            resource.data(), &Resource::loading,
+            result, &ScriptableResource::loadingChanged);
+        result->_loadedConnection = connect(
+            resource.data(), &Resource::loaded,
+            result, &ScriptableResource::loadedChanged);
+        result->_finishedConnection = connect(
+            resource.data(), &Resource::finished,
+            result, &ScriptableResource::finished);
+    }
+
+    return result;
+}
+
 ResourceCache::ResourceCache(QObject* parent) : QObject(parent) {
     auto nodeList = DependencyManager::get<NodeList>();
     if (nodeList) {
@@ -219,7 +305,7 @@ QVariantList ResourceCache::getResourceList() {
 
     return list;
 }
-
+ 
 void ResourceCache::setRequestLimit(int limit) {
     _requestLimit = limit;
 
@@ -272,6 +358,7 @@ QSharedPointer<Resource> ResourceCache::getResource(const QUrl& url, const QUrl&
                               getResource(fallback, QUrl(), true) : QSharedPointer<Resource>(), delayLoad, extra);
     resource->setSelf(resource);
     resource->setCache(this);
+    connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize);
     {
         QWriteLocker locker(&_resourcesLock);
         _resources.insert(url, resource);
@@ -357,8 +444,13 @@ void ResourceCache::removeResource(const QUrl& url, qint64 size) {
     _totalResourcesSize -= size;
 }
 
-void ResourceCache::updateTotalSize(const qint64& oldSize, const qint64& newSize) {
-    _totalResourcesSize += (newSize - oldSize);
+void ResourceCache::updateTotalSize(const qint64& deltaSize) {
+    _totalResourcesSize += deltaSize;
+
+    // Sanity checks
+    assert(_totalResourcesSize >= 0);
+    assert(_totalResourcesSize < (1024 * BYTES_PER_GIGABYTES));
+
     emit dirty();
 }
  
@@ -543,7 +635,7 @@ void Resource::finishedLoading(bool success) {
 }
 
 void Resource::setSize(const qint64& bytes) {
-    QMetaObject::invokeMethod(_cache.data(), "updateTotalSize", Q_ARG(qint64, _bytes), Q_ARG(qint64, bytes));
+    emit updateSize(bytes - _bytes);
     _bytes = bytes;
 }
 
@@ -569,8 +661,11 @@ void Resource::makeRequest() {
     }
     
     qCDebug(networking).noquote() << "Starting request for:" << _url.toDisplayString();
+    emit loading();
+
+    connect(_request, &ResourceRequest::progress, this, &Resource::onProgress);
+    connect(this, &Resource::onProgress, this, &Resource::handleDownloadProgress);
 
-    connect(_request, &ResourceRequest::progress, this, &Resource::handleDownloadProgress);
     connect(_request, &ResourceRequest::finished, this, &Resource::handleReplyFinished);
 
     _bytesReceived = _bytesTotal = _bytes = 0;
diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h
index ed3dbf69b6..b81c69c079 100644
--- a/libraries/networking/src/ResourceCache.h
+++ b/libraries/networking/src/ResourceCache.h
@@ -24,9 +24,12 @@
 #include <QtCore/QWeakPointer>
 #include <QtCore/QReadWriteLock>
 #include <QtCore/QQueue>
+
 #include <QtNetwork/QNetworkReply>
 #include <QtNetwork/QNetworkRequest>
 
+#include <QScriptEngine>
+
 #include <DependencyManager.h>
 
 #include "ResourceManager.h"
@@ -50,7 +53,7 @@ static const qint64 DEFAULT_UNUSED_MAX_SIZE = 100 * BYTES_PER_MEGABYTES;
 static const qint64 DEFAULT_UNUSED_MAX_SIZE = 1024 * BYTES_PER_MEGABYTES;
 #endif
 static const qint64 MIN_UNUSED_MAX_SIZE = 0;
-static const qint64 MAX_UNUSED_MAX_SIZE = 10 * BYTES_PER_GIGABYTES;
+static const qint64 MAX_UNUSED_MAX_SIZE = MAXIMUM_CACHE_SIZE;
 
 // We need to make sure that these items are available for all instances of
 // ResourceCache derived classes. Since we can't count on the ordering of
@@ -78,6 +81,61 @@ private:
     QList<QWeakPointer<Resource>> _loadingRequests;
 };
 
+/// Wrapper to expose resources to JS/QML
+class ScriptableResource : public QObject {
+    Q_OBJECT
+    Q_PROPERTY(QUrl url READ getUrl)
+    Q_PROPERTY(int state READ getState NOTIFY stateChanged)
+
+public:
+    enum State {
+        QUEUED,
+        LOADING,
+        LOADED,
+        FINISHED,
+        FAILED,
+    };
+    Q_ENUM(State)
+
+    ScriptableResource(const QUrl& url);
+    virtual ~ScriptableResource() = default;
+
+    Q_INVOKABLE void release();
+
+    const QUrl& getUrl() const { return _url; }
+    int getState() const { return (int)_state; }
+    const QSharedPointer<Resource>& getResource() const { return _resource; }
+
+    bool isInScript() const;
+    void setInScript(bool isInScript);
+
+signals:
+    void progressChanged(uint64_t bytesReceived, uint64_t bytesTotal);
+    void stateChanged(int state);
+
+private slots:
+    void loadingChanged();
+    void loadedChanged();
+    void finished(bool success);
+
+private:
+    void disconnectHelper();
+
+    friend class ResourceCache;
+
+    // Holds a ref to the resource to keep it in scope
+    QSharedPointer<Resource> _resource;
+
+    QMetaObject::Connection _progressConnection;
+    QMetaObject::Connection _loadingConnection;
+    QMetaObject::Connection _loadedConnection;
+    QMetaObject::Connection _finishedConnection;
+
+    QUrl _url;
+    State _state{ QUEUED };
+};
+
+Q_DECLARE_METATYPE(ScriptableResource*);
 
 /// Base class for resource caches.
 class ResourceCache : public QObject {
@@ -121,12 +179,23 @@ public slots:
     void checkAsynchronousGets();
 
 protected slots:
-    void updateTotalSize(const qint64& oldSize, const qint64& newSize);
+    void updateTotalSize(const qint64& deltaSize);
+
+    // 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);
 
 private slots:
     void clearATPAssets();
 
 protected:
+    // Prefetches a resource to be held by the QScriptEngine.
+    // Pointers created through this method should be owned by the caller,
+    // which should be a QScriptEngine with ScriptableResource registered, so that
+    // the QScriptEngine will delete the pointer when it is garbage collected.
+    Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr); }
+
     /// Loads a resource 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 resource immediately; wait until load is first requested
@@ -231,6 +300,9 @@ public:
     const QUrl& getURL() const { return _url; }
 
 signals:
+    /// Fired when the resource begins downloading.
+    void loading();
+
     /// Fired when the resource has been downloaded.
     /// This can be used instead of downloadFinished to access data before it is processed.
     void loaded(const QByteArray request);
@@ -244,6 +316,12 @@ signals:
     /// Fired when the resource is refreshed.
     void onRefresh();
 
+    /// Fired on progress updates.
+    void onProgress(uint64_t bytesReceived, uint64_t bytesTotal);
+
+    /// Fired when the size changes (through setSize).
+    void updateSize(qint64 deltaSize);
+
 protected slots:
     void attemptRequest();
 
@@ -280,21 +358,26 @@ private slots:
     void handleReplyFinished();
 
 private:
+    friend class ResourceCache;
+    friend class ScriptableResource;
+    
     void setLRUKey(int lruKey) { _lruKey = lruKey; }
     
     void makeRequest();
     void retry();
     void reinsert();
+
+    bool isInScript() const { return _isInScript; }
+    void setInScript(bool isInScript) { _isInScript = isInScript; }
     
-    friend class ResourceCache;
-    
-    ResourceRequest* _request = nullptr;
-    int _lruKey = 0;
-    QTimer* _replyTimer = nullptr;
-    qint64 _bytesReceived = 0;
-    qint64 _bytesTotal = 0;
-    qint64 _bytes = 0;
-    int _attempts = 0;
+    ResourceRequest* _request{ nullptr };
+    int _lruKey{ 0 };
+    QTimer* _replyTimer{ nullptr };
+    qint64 _bytesReceived{ 0 };
+    qint64 _bytesTotal{ 0 };
+    qint64 _bytes{ 0 };
+    int _attempts{ 0 };
+    bool _isInScript{ false };
 };
 
 uint qHash(const QPointer<QObject>& value, uint seed = 0);
diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp
index 15f3ebb985..fc8b581ffe 100644
--- a/libraries/script-engine/src/ScriptEngine.cpp
+++ b/libraries/script-engine/src/ScriptEngine.cpp
@@ -270,6 +270,48 @@ static void resultHandlerFromScriptValue(const QScriptValue& value, AnimVariantR
     assert(false);
 }
 
+// Templated qScriptRegisterMetaType fails to compile with raw pointers
+using ScriptableResourceRawPtr = ScriptableResource*;
+
+static QScriptValue scriptableResourceToScriptValue(QScriptEngine* engine, const ScriptableResourceRawPtr& resource) {
+    // The first script to encounter this resource will track its memory.
+    // In this way, it will be more likely to GC.
+    // This fails in the case that the resource is used across many scripts, but
+    // in that case it would be too difficult to tell which one should track the memory, and
+    // this serves the common case (use in a single script).
+    auto data = resource->getResource();
+    if (data && !resource->isInScript()) {
+        resource->setInScript(true);
+        QObject::connect(data.data(), SIGNAL(updateSize(qint64)), engine, SLOT(updateMemoryCost(qint64)));
+    }
+
+    auto object = engine->newQObject(
+        const_cast<ScriptableResourceRawPtr>(resource),
+        QScriptEngine::ScriptOwnership);
+    return object;
+}
+
+static void scriptableResourceFromScriptValue(const QScriptValue& value, ScriptableResourceRawPtr& resource) {
+    resource = static_cast<ScriptableResourceRawPtr>(value.toQObject());
+}
+
+static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {
+    auto prototype = engine->newObject();
+
+    // Expose enum State to JS/QML via properties
+    QObject* state = new QObject(engine);
+    state->setObjectName("ResourceState");
+    auto metaEnum = QMetaEnum::fromType<ScriptableResource::State>();
+    for (int i = 0; i < metaEnum.keyCount(); ++i) {
+        state->setProperty(metaEnum.key(i), metaEnum.value(i));
+    }
+
+    auto prototypeState = engine->newQObject(state, QScriptEngine::QtOwnership, QScriptEngine::ExcludeSlots | QScriptEngine::ExcludeSuperClassMethods);
+    prototype.setProperty("State", prototypeState);
+
+    return prototype;
+}
+
 void ScriptEngine::init() {
     if (_isInitialized) {
         return; // only initialize once
@@ -327,11 +369,16 @@ void ScriptEngine::init() {
     registerGlobalObject("Vec3", &_vec3Library);
     registerGlobalObject("Mat4", &_mat4Library);
     registerGlobalObject("Uuid", &_uuidLibrary);
-    registerGlobalObject("AnimationCache", DependencyManager::get<AnimationCache>().data());
     registerGlobalObject("Messages", DependencyManager::get<MessagesClient>().data());
     qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue);
     qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue);
 
+    // Scriptable cache access
+    auto resourcePrototype = createScriptableResourcePrototype(this);
+    globalObject().setProperty("Resource", resourcePrototype);
+    setDefaultPrototype(qMetaTypeId<ScriptableResource*>(), resourcePrototype);
+    qScriptRegisterMetaType(this, scriptableResourceToScriptValue, scriptableResourceFromScriptValue);
+
     // constants
     globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE)));
 
@@ -793,6 +840,12 @@ void ScriptEngine::callAnimationStateHandler(QScriptValue callback, AnimVariantM
     }
 }
 
+void ScriptEngine::updateMemoryCost(const qint64& deltaSize) {
+    if (deltaSize > 0) {
+        reportAdditionalMemoryCost(deltaSize);
+    }
+}
+
 void ScriptEngine::timerFired() {
     QTimer* callingTimer = reinterpret_cast<QTimer*>(sender());
     CallbackData timerData = _timerFunctionMap.value(callingTimer);
diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h
index e8ce00c66c..175a3f1f1c 100644
--- a/libraries/script-engine/src/ScriptEngine.h
+++ b/libraries/script-engine/src/ScriptEngine.h
@@ -131,6 +131,8 @@ public:
     Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const MouseEvent& event);
     Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision);
 
+    Q_INVOKABLE void requestGarbageCollection() { collectGarbage(); }
+
     ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
     // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts
     Q_INVOKABLE void stop();
@@ -156,6 +158,7 @@ public:
 
 public slots:
     void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler);
+    void updateMemoryCost(const qint64&);
 
 signals:
     void scriptLoaded(const QString& scriptFilename);
diff --git a/script-archive/theBird.js b/script-archive/theBird.js
index 02b2e7fc5d..4adc6e3968 100644
--- a/script-archive/theBird.js
+++ b/script-archive/theBird.js
@@ -20,8 +20,6 @@ for (i = 0; i < l; i++) {
     print(roles[i]);
 }
 
-MyAvatar.prefetchAnimation(THE_BIRD_RIGHT_URL);
-
 // replace point animations with the bird!
 MyAvatar.overrideRoleAnimation("rightHandPointIntro", THE_BIRD_RIGHT_URL, 30, false, 0, 12);
 MyAvatar.overrideRoleAnimation("rightHandPointHold", THE_BIRD_RIGHT_URL, 30, false, 12, 12);
diff --git a/scripts/developer/tests/scriptableResource/lib.js b/scripts/developer/tests/scriptableResource/lib.js
new file mode 100644
index 0000000000..5241d0968e
--- /dev/null
+++ b/scripts/developer/tests/scriptableResource/lib.js
@@ -0,0 +1,99 @@
+//
+//  lib.js
+//  scripts/developer/tests/scriptableResource 
+//
+//  Created by Zach Pomerantz on 4/20/16.
+//  Copyright 2016 High Fidelity, Inc.
+//
+//  Preloads textures to play a simple movie, plays it, and frees those textures.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+var NUM_FRAMES = 158; // 158 available
+var FRAME_RATE = 30;  // 30  default
+
+function getFrame(callback) {
+    // A model exported from blender with a texture named 'Picture' on one face.  
+    var FRAME_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pictureFrame/finalFrame.fbx";
+
+    var model = ModelCache.prefetch(FRAME_URL);
+    if (model.state === Resource.State.FINISHED) {
+        makeFrame(Resource.State.FINISHED);
+    } else {
+        model.stateChanged.connect(makeFrame);
+    }
+
+    function makeFrame(state) {
+        if (state == Resource.State.FAILED) { throw "Failed to load frame"; }
+        if (state != Resource.State.FINISHED) { return; }
+
+        var pictureFrameProperties = {
+            name: 'scriptableResourceTest Picture Frame',
+            type: 'Model',
+            position: getPosition(),
+            modelURL: FRAME_URL,
+            dynamic: true,
+        };
+
+        callback(Entities.addEntity(pictureFrameProperties));
+    }
+
+    function getPosition() {
+        // Always put it 5 meters in front of you
+        var position = MyAvatar.position;
+        var yaw = MyAvatar.bodyYaw + MyAvatar.getHeadFinalYaw();
+        var rads = (yaw / 180) * Math.PI;
+
+        position.y += 0.5;
+        position.x += - 5 * Math.sin(rads);
+        position.z += - 5 * Math.cos(rads);
+
+        print(JSON.stringify(position));
+        return position;
+    }
+}
+
+function prefetch(callback) {
+    // A folder full of individual frames.
+    var MOVIE_URL = "http://hifi-content.s3.amazonaws.com/james/vidtest/";
+
+    var frames = [];
+
+    var numLoading = 0;
+    for (var i = 1; i <= NUM_FRAMES; ++i) {
+        var padded = pad(i, 3);
+        var filepath = MOVIE_URL + padded + '.jpg';
+        var texture = TextureCache.prefetch(filepath);
+        frames.push(texture);
+        if (!texture.state == Resource.State.FINISHED) {
+            numLoading++;
+            texture.stateChanged.connect(function(state) {
+                if (state == Resource.State.FAILED || state == Resource.State.FINISHED) {
+                    --numLoading;
+                    if (!numLoading) { callback(frames); }
+                }
+            });
+        }
+    }
+    if (!numLoading) { callback(frames); }
+
+    function pad(num, size) { // left-pad num with zeros until it is size digits
+        var s = num.toString();
+        while (s.length < size) { s = "0" + s; }
+        return s;
+    }
+}
+
+function play(model, frames, callback) {
+    var frame = 0;
+    var movieInterval = Script.setInterval(function() {
+        Entities.editEntity(model, { textures: JSON.stringify({ Picture: frames[frame].url }) });
+        if (++frame >= frames.length) {
+            Script.clearInterval(movieInterval);
+            callback();
+        }
+    }, 1000 / FRAME_RATE);
+}
+
diff --git a/scripts/developer/tests/scriptableResource/movieTest.js b/scripts/developer/tests/scriptableResource/movieTest.js
new file mode 100644
index 0000000000..61b2bf7942
--- /dev/null
+++ b/scripts/developer/tests/scriptableResource/movieTest.js
@@ -0,0 +1,42 @@
+//
+//  testMovie.js
+//  scripts/developer/tests/scriptableResource 
+//
+//  Created by Zach Pomerantz on 4/27/16.
+//  Copyright 2016 High Fidelity, Inc.
+//
+//  Preloads textures, plays them on a frame model, and unloads them.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+var entity;
+
+Script.include([
+        '../../../developer/utilities/cache/cacheStats.js',
+        'lib.js',
+], function() {
+    getFrame(function(frame) {
+        entity = frame;
+        prefetch(function(frames) {
+            play(frame, frames, function() {
+                // Delete each texture, so the next garbage collection cycle will release them.
+
+                // Setting frames = null breaks the reference,
+                // but will not delete frames from the calling scope.
+                // Instead, we must mutate it in-place to free its elements for GC
+                // (assuming the elements are not held elsewhere).
+                while (frames.length) { frames.pop(); }
+
+                // Alternatively, forcibly release each texture without relying on GC.
+                // frames.forEach(function(texture) { texture.release(); });
+
+                Entities.deleteEntity(entity);
+                Script.requestGarbageCollection();
+            });
+        });
+    });
+});
+
+Script.scriptEnding.connect(function() { entity && Entities.deleteEntity(entity); });
diff --git a/scripts/developer/tests/scriptableResource/prefetchTest.js b/scripts/developer/tests/scriptableResource/prefetchTest.js
new file mode 100644
index 0000000000..cda805967e
--- /dev/null
+++ b/scripts/developer/tests/scriptableResource/prefetchTest.js
@@ -0,0 +1,33 @@
+//
+//  testPrefetch.js
+//  scripts/developer/tests/scriptableResource 
+//
+//  Created by Zach Pomerantz on 4/27/16.
+//  Copyright 2016 High Fidelity, Inc.
+//
+//  Preloads textures and unloads them.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+Script.include([
+    '../../../developer/utilities/cache/cacheStats.js',
+    'lib.js',
+], function() {
+    prefetch(function(frames) {
+        // Delete each texture, so the next garbage collection cycle will release them.
+
+        // Setting frames = null breaks the reference,
+        // but will not delete frames from the calling scope.
+        // Instead, we must mutate it in-place to free its elements for GC
+        // (assuming the elements are not held elsewhere).
+        while (frames.length) { frames.pop(); }
+
+        // Alternatively, forcibly release each texture without relying on GC.
+        // frames.forEach(function(texture) { texture.release(); });
+
+        Script.requestGarbageCollection();
+    });
+});
+
diff --git a/scripts/system/away.js b/scripts/system/away.js
index 687345a5e1..932efd6b60 100644
--- a/scripts/system/away.js
+++ b/scripts/system/away.js
@@ -46,8 +46,8 @@ var AWAY_INTRO = {
     endFrame: 83.0
 };
 
-// prefetch the kneel animation so it's resident in memory when we need it.
-MyAvatar.prefetchAnimation(AWAY_INTRO.url);
+// prefetch the kneel animation and hold a ref so it's always resident in memory when we need it.
+var _animation = AnimationCache.prefetch(AWAY_INTRO.url);
 
 function playAwayAnimation() {
     MyAvatar.overrideAnimation(AWAY_INTRO.url, AWAY_INTRO.playbackRate, AWAY_INTRO.loopFlag, AWAY_INTRO.startFrame, AWAY_INTRO.endFrame);