LinkedinGitHub
Close

Asynchronous texture loading in Unity

Most of the time, Unity is great, but sometimes, you have to get your hands dirty. And this is the case with texture loading. Sure, it works great, if you’re using the built-in system, but that’s not possible in some cases.

Imagine this scenario: You’re making a 2D game, that has, say, 15 levels, and each level has a background. Since you’re making your game for mobile devices you want to keep your game as compact as possible. 10 levels in you’re pretty happy with yourself, the install file is roughly 20mb and that seems reasonable. But you notice that once you install your game on an iPad it suddenly becomes 600mb and that just won’t work.

So what’s going on here?

Well, when your game is installed, Unity will uncompress all of your images into the format you selected. There are a few problems here:

  • Only POT (power-of-two) textures can be compressed into PVRTC.
  • Even if you’d get your sprites into a POT sized texture, most of the time they look so horrible you’ll want to gouge your eyes out.
  • Using Truecolor (uncompressed) will make your sprites take up a lot of space on the device, 15-20mb per background!!!

If only there was a way to keep the quality and size of those nice .png files… Well there, is the Texture2D class has a nice method to load raw texture data, and it does png decompression too. It would look like this:

TextAsset loadedAsset = Resources.Load<TextAsset>(!myTexture.png!);

Texture2D tex = new Texture2D(0, 0, TextureFormat.RGBA32, false, false);
tex.LoadImage(loadedAsset.bytes);

But, there is one very big problem with this method. It’s not asynchronous. So every time you run this baby to load a background, your game will freeze for up to a second on bigger textures. And we don’t want that.
(Yes, I know you can do Resources.LoadAsync, but the main problem here is Texture2D.LoadImage, and no, you can’t run it in a separate thread, it has to be the UI thread.)

Currently, there is no way to asynchronously load a texture in Unity. At least, there isn’t a build-in way to do it 🙂

There is, however, a way to do it with some good old native methods. You can check the full discussion on this post, but here is the gist of it:

You’ll need a native plugin (located at Assets/Plugins/iOS/FastTex2D.mm):

//(c) Brian Chasalow 2014 - brian@chasalow.com
// Edits by Miha Krajnc
#import &lt;GLKit/GLKit.h&gt;
 
extern "C" {
 
  typedef void (*TextureLoadedCallback)(int texID, int originalUUID, int w, int h);
  static TextureLoadedCallback textureLoaded;
  static GLKTextureLoader* asyncLoader = nil;
 
  void RegisterFastTexture2DCallbacks(void (*cb)(int texID, int originalUUID, int w, int h)){
      textureLoaded = *cb;
  }
 
  void CreateFastTexture2DFromAssetPath(const char* assetPath, int uuid, bool resize, int resizeW, int resizeH){
      @autoreleasepool {
          NSDictionary* options = [NSDictionary dictionaryWithObjectsAndKeys:
                                   [NSNumber numberWithBool:YES],
                                   GLKTextureLoaderOriginBottomLeft,
                                   nil];
 
          NSString* assetPathString = [NSString stringWithCString: assetPath encoding:NSUTF8StringEncoding];
 
          if(asyncLoader == nil) {
              asyncLoader = [[GLKTextureLoader alloc] initWithSharegroup:[[EAGLContext currentContext] sharegroup]];
          }
 
          if(resize){
              // UIImage* img = [UIImage imageWithContentsOfFile:assetPathString];
              // __block UIImage* smallerImg = [img resizedImage:CGSizeMake(resizeW, resizeH) interpolationQuality:kCGInterpolationDefault ];
              //
              // [asyncLoader textureWithCGImage:[smallerImg CGImage]
              //                         options:options
              //                           queue:NULL
              //               completionHandler:^(GLKTextureInfo *textureInfo, NSError *outError) {
              //                   if(outError){
              //                     smallerImg = nil;
              //                     NSLog(@&quot;got error creating texture at path: %@. error: %@ &quot;, assetPathString,[outError localizedDescription] );
              //                       textureLoaded(-1, uuid, 0, 0);
              //                   }
              //                   else{
              //                       textureLoaded(textureInfo.name, uuid, resizeW, resizeH);
              //                   }
              //               }];
 
          } else {
              [asyncLoader textureWithContentsOfFile:assetPathString
                           options:options
                           queue:NULL
                           completionHandler:^(GLKTextureInfo *textureInfo, NSError *outError) {
                    if(outError){
                        NSLog(@&quot;got error creating texture at path: %@. error: %@ &quot;, assetPathString,[outError localizedDescription] );
                        NSLog(@&quot;returning texID -1 &quot;);
                        textureLoaded(-1, uuid, 0, 0);
                    }
                    else
                    {
                      //this will get returned on the main thread cuz the queue above is NULL
                      textureLoaded(textureInfo.name, uuid, textureInfo.width, textureInfo.height);
                    }
                }];
          }
      }
  }
 
  void DeleteFastTexture2DAtTextureID(int texID){
      @autoreleasepool {
          GLuint texIDGL = (GLuint)texID;
          if(texIDGL &gt; 0){
              if(glIsTexture(texIDGL)){
                  NSLog(@&quot;deleting a texture because it's a texture. %i&quot;, texIDGL);
                  glDeleteTextures(1, &amp;texIDGL);
              }
          }
      }
  }
}

 

And a C# wrapper:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.IO;
using System.Runtime.InteropServices;
 
public class FastTexture2D : ScriptableObject
{
    //(c) Brian Chasalow 2014 - brian@chasalow.com
    // Revisions by Miha Krajnc
    [AttributeUsage (AttributeTargets.Method)]
    public sealed class MonoPInvokeCallbackAttribute : Attribute
    {
        public MonoPInvokeCallbackAttribute (Type t)
        {
        }
    }
 
    [DllImport ("__Internal")]
    private static extern void DeleteFastTexture2DAtTextureID (int id);
 
    [DllImport ("__Internal")]
    private static extern void CreateFastTexture2DFromAssetPath (string assetPath, int uuid, bool resize, int resizeW, int resizeH);
 
    [DllImport ("__Internal")]
    private static extern void RegisterFastTexture2DCallbacks (TextureLoadedCallback callback);
 
    public static void CreateFastTexture2D (string path, int uuid, bool resize, int resizeW, int resizeH)
    {
        #if UNITY_EDITOR
        #elif UNITY_IOS
        CreateFastTexture2DFromAssetPath(path, uuid, resize, resizeW, resizeH);
        #endif
    }
 
    public static void CleanupFastTexture2D (int texID)
    {
        #if UNITY_EDITOR
        #elif UNITY_IOS
        DeleteFastTexture2DAtTextureID(texID);
        #endif
    }
 
 
    private static int tex2DCount = 0;
    private static Dictionary<int, FastTexture2D> instances;
 
    public static Dictionary<int, FastTexture2D> Instances {
        get {
            if (instances == null) {
                instances = new Dictionary<int, FastTexture2D> ();
            }
            return instances;
        }
    }
 
    [SerializeField]
    public string url;
    [SerializeField]
    public int uuid;
    [SerializeField]
    public bool resize;
    [SerializeField]
    public int w;
    [SerializeField]
    public int h;
    [SerializeField]
    public int glTextureID;
    [SerializeField]
    private Texture2D nativeTexture;
 
    public Texture2D NativeTexture{ get { return nativeTexture; } }
 
    [SerializeField]
    public bool isLoaded = false;
 
    public delegate void TextureLoadedCallback (int nativeTexID, int original_uuid, int w, int h);
 
    [MonoPInvokeCallback (typeof(TextureLoadedCallback))]
    public static void TextureLoaded (int nativeTexID, int original_uuid, int w, int h)
    {
        if (Instances.ContainsKey (original_uuid) && nativeTexID > -1) {
            FastTexture2D tex = Instances [original_uuid];
            tex.glTextureID = nativeTexID;
            tex.nativeTexture = Texture2D.CreateExternalTexture (w, h, TextureFormat.ARGB32, false, true, (System.IntPtr)nativeTexID);
            tex.nativeTexture.UpdateExternalTexture ((System.IntPtr)nativeTexID);
            tex.isLoaded = true;
            tex.OnFastTexture2DLoaded (tex);
        }
    }
 
    private Action<FastTexture2D> OnFastTexture2DLoaded;
 
    protected void InitFastTexture2D (string _url, int _uuid, bool _resize, int _w, int _h, Action<FastTexture2D> callback)
    {
        this.url = _url;
        this.uuid = _uuid;
        this.resize = _resize;
        this.w = _w;
        this.h = _h;
        this.glTextureID = -1;
        this.OnFastTexture2DLoaded = callback;
        this.isLoaded = false;
    }
 
    private static bool registeredCallbacks = false;
 
    private static void RegisterTheCallbacks ()
    {
        if (!registeredCallbacks) {
            registeredCallbacks = true;
            #if UNITY_IOS
            if (Application.platform == RuntimePlatform.IPhonePlayer)
                RegisterFastTexture2DCallbacks (TextureLoaded);
            #endif
 
        }
    }
 
 
    //dimensions options: if resize is false, w/h are not used. if true, it will downsample to provided dimensions.
    //to create a new texture, call this with the file path of the texture, resize parameters,
    //and a callback to be notified when the texture is loaded.
    public static FastTexture2D CreateFastTexture2D (string url, bool resize, int assetW, int assetH, Action<FastTexture2D> callback)
    {
        #if !UNITY_IOS
        if(tex2DCount == 9999){
            // Do nothing - to eliminate the editor warning
        }
 
        WWW ld = new WWW("file://" + url);
        while(!ld.isDone);
 
        Texture2D t2d = new Texture2D(0, 0, TextureFormat.RGBA32, false);
        t2d.LoadImage(ld.bytes);
 
        FastTexture2D ft = ScriptableObject.CreateInstance<FastTexture2D>();
        ft.nativeTexture = t2d;
        callback(ft);
        return ft;
        #else
 
        //register that you want a callback when it's been created.
        RegisterTheCallbacks ();
        //the uuid is the instance count at time of creation. you pass this into the method to grab the gl texture, and it returns the gl texture with this uuid
        int uuid = tex2DCount;
        tex2DCount = (tex2DCount + 1) % int.MaxValue;
 
        FastTexture2D tex2D = ScriptableObject.CreateInstance<FastTexture2D> ();
        tex2D.InitFastTexture2D (url, uuid, resize, assetW, assetH, callback);
        //call into the plugin to create the thing
        CreateFastTexture2D (tex2D.url, tex2D.uuid, tex2D.resize, tex2D.w, tex2D.h);
 
        //add the instance to the list
        Instances.Add (uuid, tex2D);
 
        //return the instance, someone might want it (but they'll get it with the callback soon anyway)
        return tex2D;
        #endif
    }
 
    private void CleanupTexture ()
    {
        isLoaded = false;
 
        //delete the gl texture
        if (glTextureID != -1)
            CleanupFastTexture2D (glTextureID);
        glTextureID = -1;
 
        //destroy the wrapper object
        if (nativeTexture)
            Destroy (nativeTexture);
 
        //remove it from the list so further callbacks dont try to find it
        if (Instances.ContainsKey (this.uuid))
            Instances.Remove (this.uuid);
    }
 
    //to destroy a FastTexture2D object, you call Destroy() on it.
    public void OnDestroy ()
    {
        CleanupTexture ();
    }
}

 

You won’t need any special cases for other platforms, when using FastTexture2D, but the optimised asynchronous method will only work on iOS.

Note: You will also need to force Unity to use OpenGL for this to work, you can do it under PlayerSettings -> Other Settings -> Auto Graphics API

Big thanks to Brian Chasalow for showing us this method!

1 thought on “Asynchronous texture loading in Unity

Leave a Reply

Your email address will not be published. Required fields are marked *