Async texture import in Unity engine

Jan 3, 2022 · 3 mins read

Async texture import in Unity engine

TLDR: Here’s a repository containing code for async texture import in Unity: https://codeberg.org/matiaslavik/unity-async-textureimport

The problem

In Unity, the standard way of importing textures is through the ImageConversion.LoadImage (previously Texture2D.Load) function. However, this function has some limitations. One of these is the lack of async import. You are forced to use it on the main thread, and it will block execution until file reading, import and mipmap generation is done. This can result in long stalls, which is unacceptable for most games and realtime applications.

The solution

Since this part of Unity’s source code is not open sourced, we are forced to re-invent the wheel and do the import ourselves. To do this, we can use the amazing FreeImage library. FreeImage is a library for image import/export and manipulation, written in C.

Since FreeImage is a native library written in C and not C#, we need to create a C# wrapper for it. In Unity this is quite simple. You just need to create a class with static extern function declarations for all the native FreeImage functions you want to call, and mark them with the DllImport attribute.

[DllImport(FreeImageLibrary, EntryPoint = "FreeImage_OpenMemory")]
public static extern IntPtr FreeImage_OpenMemory(IntPtr data, uint size_in_bytes);

See FreeImage.cs for all FreeImage wrappers used in the project.

We import the texture using the FreeImage_Load function. This is done in the ImportTextureFromFile function.

// Load from file
IntPtr texHandle = FreeImage.FreeImage_Load(format, texturePath, 0);

Next we can use the FreeImage_ConvertToRawBits function to read the texture data into a byte array:

uint width = FreeImage.FreeImage_GetWidth(texHandle);
uint height = FreeImage.FreeImage_GetHeight(texHandle);
uint size = width * height * 4;

byte[] data = new byte[size];
FreeImage.FreeImage_ConvertToRawBits(Marshal.UnsafeAddrOfPinnedArrayElement(data, 0), texHandle, (int)width * 4, 32, 0, 0, 0, false

We can then create a new Texture2D and pass the raw texture data to it using the LoadRawTextureData function. However there is one problem left: We don’t have any mipmaps, and the LoadRawTextureData function expects you to create the mipmaps on your own (or not use any mipmaps).

Mipmaps

First of all, what is a mipmap? A mipmap is simply an image sequence of the same image at different scales. For example, if your image has a dimension of 256x256, then the mipmap will contain the image at the following resolutions: 256x256, 128x128, 64x64, etc. (resolution is recursively divided by 2). This is used for rendering objects at far distances. If you don’t use mipmaps, then high resolution textures will look noisy at far distances.

Again, FreeImage comes to rescue. Using the FreeImage_Rescale function, we can downscale the image recursively to create a mipnap. This is done in the GenerateMipMaps function.

Texture creation

Finally we can create the Texture2D instance and pass it the texture data through the LoadRawTextureData function. This is done in the CreateTexture function.

Texture2D tex = new Texture2D(texData.width, texData.height, TextureFormat.BGRA32, texData.mipLevels, false);
tex.filterMode = FilterMode.Trilinear;
tex.LoadRawTextureData(texData.data);
tex.Apply(false, true);

Note: We need to call Texture2D.Apply with the updateMipmaps parameter set to false, since we created the mipmaps ourselves.

Sharing is caring!