Using WWW to Avoid Compression

UsingWWWToAvoidCompression

Unity3D – I love just how scriptable everything in Unity can be and how easy it is to develop your own tools, but I have run into issues when trying to get at a raw version of an asset in tools. For all my searching even at editor time you are working with an already imported asset. This became a problem when I needed to be able to bake textures into atlases and then flatten their material. Even though my tools only ever needed to work on the editor side Unity made me jump through some hopes to avoid compression artifacts entering the texture.

rawVetc

There appeared to be to easy(-ish) solutions; set up the project with a sudo uncompressed and read/write enabled texture source within the assets somewhere or have the tool change the import settings, do stuff then set everything back. Having the extra source textures would be very cumbersome and bloat the repo size. There was always the danger of accidentally hooking up the uncompressed versions of the texture to an in game asset.

So for a long time the solution of having tools run through these steps to avoid compression was good enough:

  • Gather up the textures we were processing
  • Change there import settings
  • Force re-import with “ImportAssetOptions.DontDownloadFromCacheServer”
  • Do the texture processing
  • Force re-import with old asset settings

After upgrading to Unity 5 these steps taking significantly longer than before. Part of it was a change in compression quality settings so now normal compressing Unity 5 was actually the same as Best in previous versions. Different versions of texture compressor for android and iOS that appear to be slower on pc for some reason along with some misc changes including Unity’s p4 integration all greatly slowed down the texture import and re-import times.

All this seemed really silly and thankfully there is a way around it if you are using .png(s) or .jpg(s). Unity’s WWW script class includes a WWW.texture variable for loading images from the web. This can also be used to load images off your hard drive even outside of your project!

CompressThis
I will be using this image for the loading tests. it has a source resolution of 512×512 but I have set the resolution to much smaller in Unity.

To get started the using WWW class is going to require IEnumerator methods. Since we are working in editor we are going to avoid deriving from MonoBehaviour to get our StartCoroutine() and look elsewhere. I found EditorCoroutine.cs on GitHub by benblo that adds the functionality that we are looking for.

TextureWebLoad.cs

/// <summary>
/// TextureWebLoad.cs
/// www.noobpaint.com
/// WebLoad Texture using WWW class.
/// uses EditorCoroutines by benblo
/// </summary>

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

public class TextureWebLoad
{
  public bool locked {
    get
    {
      if (null == _loadTexture)
      {
        return _locked;
      }
      else
      {
        _locked = false;
      }
      return _locked;
    }
  }
  protected bool _locked;

  public Texture2D loadTexture {get{return _loadTexture;}}
  protected Texture2D _loadTexture = null;

  protected Swing.Editor.EditorCoroutine edCoroutine = null;

  /// <summary>
  /// Reset this instance.
  /// </summary>
  public void reset ()
  {
    if (null != edCoroutine)
    {
      edCoroutine.stop();
      edCoroutine = null;
    }
    _locked = false;
    _loadTexture = null;
  }

  /// <summary>
  /// Editor safe way to call WebLoadTexture()
  /// calls EditorCoroutine and loads texture to loadTexture
  /// </summary>
  /// <param name="sourceTexture">Source texture.</param>
  /// <param name="width">Width.</param>
  /// <param name="height">Height.</param>
  public void EditorWebLoad(Texture2D sourceTexture)
  {
    if (!_locked)
    {
      _locked = true;
      edCoroutine = Swing.Editor.EditorCoroutine.start(
        WebLoadTexture(sourceTexture, 
        callback => _loadTexture = callback)
      );
    }
  }

  /// <summary>
  /// Editor safe way to call WebLoadTexture()
  /// calls EditorCoroutine and loads texture to loadTexture
  /// </summary>
  /// <param name="sourceTexture">Source texture.</param>
  /// <param name="width">Width.</param>
  /// <param name="height">Height.</param>
  public void EditorWebLoad(Texture2D sourceTexture, int width, int height)
  {
    if (!_locked)
    {
      _locked = true;
      edCoroutine = Swing.Editor.EditorCoroutine.start(
        WebLoadTexture(sourceTexture, width, height, 
        callback => _loadTexture = callback)
      );
    }
  }

  /// <summary>
  /// Loads a png or jpg texture using WWW class
  /// to avoid texture compression or gives access 
  /// to raw texture size.
  /// </summary>
  /// <returns>The load texture.</returns>
  /// <param name="texture">Texture.</param>
  /// <param name="result">Result.</param>
  public static IEnumerator WebLoadTexture (Texture2D texture, 
  System.Action<Texture2D> result)
  {
    Texture2D loadTexture = null;
    Swing.Editor.EditorCoroutine.start(
      WebLoadTexture(texture, texture.width, texture.height, 
      callback => loadTexture = callback));
    while(null == loadTexture)
    {
      yield return null;
    }

    result(loadTexture);
  }

  /// <summary>
  /// Loads a png or jpg texture using WWW class
  /// to avoid texture compression or gives access 
  /// to raw texture size.
  /// </summary>
  /// <returns>The load texture.</returns>
  /// <param name="texture">Texture.</param>
  /// <param name="width">Width.</param>
  /// <param name="height">Height.</param>
  /// <param name="result">Result.</param>
  public static IEnumerator WebLoadTexture (Texture2D texture, int width, 
  int height, System.Action<Texture2D> result)
  {
    bool doWebLoad = true;
    if (texture.name.Contains("_WebLoad"))
    {
      doWebLoad = false;
    }

    Texture2D outTexture;
    if (doWebLoad)
    {   
      string path = AssetDatabase.GetAssetPath(texture);
      if (string.IsNullOrEmpty(path))
      {
        outTexture = texture;
        doWebLoad = false;
      }
      else
      {
        path = "file://" + System.IO.Path.GetFullPath(
        AssetDatabase.GetAssetPath(texture));

        using (WWW webLoad = new WWW(path))
        {
          while (webLoad.texture == null)
          {
            yield return null;
          }
          outTexture = Texture2D.blackTexture;
          outTexture = webLoad.texture;

          outTexture.wrapMode = TextureWrapMode.Clamp;
          outTexture.Apply();

          if (outTexture.width != width || outTexture.height != height)
          {
            outTexture.ResizeAndFill(width, height);
          }
        }
      }
    }
    else
    {
      outTexture = texture;
    }

    if (doWebLoad)
    {
      while(null == outTexture)
      {
        yield return null;
      }

      outTexture.name += "_WebLoad";
    }

    result(outTexture);
  }
}

We are also going to make use of an Texture2D extension method script for to allow us to scale the image when we load. This is useful since we have access to the original scale of the image when using the web load. I go over this extension more here.

Texture2DExtensionMethods.cs

/// <summary>
/// Texture2D extension methods.
/// www.noobpaint.com
/// </summary>
using UnityEngine;
using System.Collections;

public static class Texture2DExtensionMethods
{
  /// <summary>
  /// Gets the pixel centers of a texture.
  /// </summary>
  /// <returns>The pixel centers.</returns>
  /// <param name="texture2D">Texture2 d.</param>
  public static float[,] GetPixelCenters(this Texture2D texture2D)
  {
    return GetPixelCenters(texture2D, texture2D.width, 
    texture2D.height);
  }

  /// <summary>
  /// Gets the pixel centers of texture size width by height.
  /// </summary>
  /// <returns>The pixel centers.</returns>
  /// <param name="texture2D">Texture2 d.</param>
  /// <param name="width">Width.</param>
  /// <param name="height">Height.</param>
  public static float[,] GetPixelCenters(this Texture2D texture2D, 
  int width, int height)
  {
    //generate array of offsets for get pixels
    //get uv coord at center of pixel for sampling
    float xStep = 1.0f / (width*2.0f);
    float yStep = 1.0f / (height*2.0f);
    int N = width * height;

    //generate x centers
    float[] xSteps = new float[width];
    for (int i = 0; i < width; i++)
    {
      xSteps[i] = (i * xStep * 2) + xStep;
    }
    //generate y centers 
    float[] ySteps = new float[height];
    for (int i = 0; i < height; i++)
    {
      ySteps[i] = (i * yStep * 2) + yStep;
    }
    //array of float offsets starting at bottom left going right
    //bottom to top
    float[,] offsetArray = new float[N,2];
    for (int i = 0; i < N; i++)
    {
      int x = i % width;
      int y = (i - x) / width;

      offsetArray[i,0] = xSteps[x];
      offsetArray[i,1] = ySteps[y];
    }
    return offsetArray;
  }

  /// <summary>
  /// Gets the pixels using bilinear filtering
  /// to flattened 2D array, where pixels are laid 
  /// out left to right, bottom to top.
  /// </summary>
  /// <returns>The pixels bilinear.</returns>
  /// <param name="texture">Texture.</param>
  public static Color[] GetPixelsBilinear(this Texture2D texture)
  {
    return GetPixelsBilinear(texture, texture.width, texture.height);
  }

  /// <summary>
  /// Gets the pixels using bilinear filtering
  /// to flattened 2D array, where pixels are laid 
  /// out left to right, bottom to top.
  /// </summary>
  /// <returns>The pixels bilinear.</returns>
  /// <param name="texture">Texture.</param>
  /// <param name="width">Width.</param>
  /// <param name="height">Height.</param>
  public static Color[] GetPixelsBilinear(this Texture2D texture, 
  int width, int height)
  {
    int N = width * height;

    Color[] pixels = new Color[N];

    float[,] offsetArray = texture.GetPixelCenters(width,height);

    for (int i = 0; i < N; i++)
    {
      pixels[i] = texture.GetPixelBilinear(offsetArray[i,0], 
      offsetArray[i,1]);
    }
    return pixels;
  }

  /// <summary>
  /// Resizes Texture and fills with get pixels bilinear.
  /// </summary>
  /// <returns>The and fill.</returns>
  /// <param name="texture">Texture.</param>
  /// <param name="width">Width.</param>
  /// <param name="height">Height.</param>
  public static Texture2D ResizeAndFill(this Texture2D texture, 
  int width, int height)
  {
    int N = width * height;

    Color[] pixels = new Color[N];

    pixels = texture.GetPixelsBilinear(width, height);

    texture.Resize(width, height);

    texture.SetPixels(pixels);
    texture.Apply();

    return texture;
  }

  public static Texture2D Copy(this Texture2D texture)
  {
    return Object.Instantiate<Texture2D>(texture);
  }
}

Now to set up a little editor window to check our web load.
webloadTestGUI
Remember only .png and .jpg file formats are supported by WWW.texture in Unity. What may be of interest to some of you is my use of the Update() function in EditorWindow. This is important for loading a texture with a lambda function as we cannot hold up the main OnGUI() waiting for our texture to be returned.

TextureWebLoadWindow.cs

/// <summary>
/// TextureWebLoadWindow.cs
/// www.noobpaint.com
/// Texture web load window.
/// </summary>
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

public class TextureWebLoadWindow : EditorWindow
{
  public Texture2D sourceTexture;
  public Texture2D destTexture;

  private TextureWebLoad textureWebLoad = new TextureWebLoad();

  private Vector2 _scrollPos = Vector2.zero;

  int imageRez = 128;

  enum eWindowState
  {
    normal,
    testLaod,
    reset,
  }
  private eWindowState currentState = eWindowState.normal;

  [MenuItem("Texture/TextureWebLoad")]
  private static void CreateWizard()
  {
    EditorWindow.GetWindow<TextureWebLoadWindow>("TextureWebLoad");
  }

  void OnGUI()
  {
    Color previousColor = GUI.backgroundColor;
    GUI.backgroundColor = Color.grey;
    EditorGUILayout.BeginVertical();
    EditorGUILayout.LabelField("Current State: " + currentState.ToString());
    GUI.backgroundColor = Color.green;

    sourceTexture = EditorGUILayout.ObjectField("Texture", sourceTexture, 
    typeof(Texture2D), false) as Texture2D;
    if (sourceTexture != null)
    {
      EditorGUILayout.IntField("width", sourceTexture.width);
      EditorGUILayout.IntField("height", sourceTexture.height);
    }

    _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);

    if (sourceTexture != null)
    {
      GUILayout.Label(sourceTexture);
      imageRez = EditorGUILayout.IntField(imageRez);
      if(GUILayout.Button("Test Load resize") &amp&amp 
      eWindowState.normal == currentState)
      {
        // do test load in update loop
        currentState = eWindowState.testLaod;
      }
      if (destTexture != null)
      {
        GUILayout.Label(destTexture);
      }
    }


    if (destTexture != null)
    {
      if (GUILayout.Button("Reset Load Texture"))
      {
        // do reset in update loop
        currentState = eWindowState.reset;
      }
    }
    EditorGUILayout.EndScrollView();
    EditorGUILayout.EndVertical();
    GUI.backgroundColor = previousColor;
  }

  void Update()
  {
    switch(currentState)
    {
    case eWindowState.normal:
      {
        if (null != textureWebLoad.loadTexture)
        {
          destTexture = textureWebLoad.loadTexture.Copy();
          textureWebLoad.reset();
        }
        break;
      }
    case eWindowState.testLaod:
      {
        if (!textureWebLoad.locked &amp&amp null != sourceTexture)
        {
          textureWebLoad.EditorWebLoad(sourceTexture, imageRez, imageRez);
        }
        if (null != textureWebLoad.loadTexture)
        {
          currentState = eWindowState.normal;
        }
        break;
      }
    case eWindowState.reset:
      {
        destTexture = null;
        textureWebLoad.reset();
        currentState = eWindowState.normal;
        break;
      }
    }
  }
}