Load a WebP image from a URL in Unity

Unity’s built-in loaders decode PNG, JPG and EXR only, so a .webp comes back blank — download the raw bytes and hand them to a WebP decoder instead.

Applies to Unity 2022.3 LTS through Unity 6.0 LTS

The reason this is confusing is that nothing fails. The request succeeds, the bytes arrive, the texture is created, and what you get is blank. There is no exception and usually no console entry — so the bug looks like it is in your material, your UI canvas or your CORS setup, and it is in none of them.

Why it comes back blank

Both of the ways that do not work
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class WhyItIsBlank : MonoBehaviour
{
    IEnumerator Load(string url)
    {
        // UnityWebRequestTexture.GetTexture decodes as it downloads — and it
        // decodes PNG and JPG. Handed a .webp it gives you a placeholder
        // texture and NO error, which is the blank quad you are looking at.
        using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url))
        {
            yield return request.SendWebRequest();

            Texture2D texture = DownloadHandlerTexture.GetContent(request);
            GetComponent<Renderer>().material.mainTexture = texture;   // blank
        }
    }

    IEnumerator AlsoBlank(string url)
    {
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            yield return request.SendWebRequest();

            var texture = new Texture2D(2, 2);

            // ImageConversion.LoadImage "Loads PNG, JPG or EXR image byte array
            // into a texture" and returns false for anything else. Most code
            // ignores the bool, keeps the untouched 2x2 texture, and reports a
            // blank image rather than a decode failure.
            if (!texture.LoadImage(request.downloadHandler.data))
            {
                Debug.LogError("Not PNG, JPG or EXR — nothing was decoded.");
            }
        }
    }
}

Neither of these is wrong about the network. Both of them are wrong about the format.

Unity's documentation is unambiguous once you go looking: ImageConversion.LoadImage “loads PNG, JPG or EXR image byte array into a texture” and returns false for anything else. WebP is not on that list and never has been. UnityWebRequestTexture.GetTexture runs the same decoder inside the download handler, so it fails the same way — it just fails somewhere you cannot see the boolean.

So the first thing to fix is the silence: check the return value. A LoadImage that returns false turns “my image is blank” into “this file is not a format Unity decodes”, which is a two-minute problem instead of a two-day one.

Download the bytes, decode them separately

Split the two jobs. UnityWebRequest.Get fetches bytes and does not try to be clever about them; the decode is then yours to route to something that understands WebP.

WebpDownload.cs
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class WebpDownload : MonoBehaviour
{
    /// <summary>
    /// Downloads the raw bytes. This part always works — WebP is only a problem
    /// at decode time, never at download time.
    /// </summary>
    public IEnumerator Download(string url, Action<byte[]> onBytes, Action<string> onError)
    {
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                onError($"{request.responseCode} {request.error}");
                yield break;
            }

            byte[] bytes = request.downloadHandler.data;

            if (!LooksLikeWebp(bytes))
            {
                // Worth checking before you blame the decoder: a CDN that
                // content-negotiates will hand a Unity client the AVIF or the
                // JPEG, not the .webp the URL names.
                onError("Server did not return a WebP file.");
                yield break;
            }

            onBytes(bytes);
        }
    }

    /// <summary>
    /// RIFF container check, per RFC 9649: "RIFF", a 4-byte length, then "WEBP".
    /// </summary>
    public static bool LooksLikeWebp(byte[] bytes)
    {
        return bytes != null
            && bytes.Length >= 12
            && bytes[0] == 'R' && bytes[1] == 'I' && bytes[2] == 'F' && bytes[3] == 'F'
            && bytes[8] == 'W' && bytes[9] == 'E' && bytes[10] == 'B' && bytes[11] == 'P';
    }
}

The header check is four lines and it has settled a lot of arguments about whose bug it is.

That LooksLikeWebp check earns its place. A CDN with content negotiation will happily serve a Unity client whatever it thinks the client wants — a JPEG, or increasingly an AVIF — from a URL that ends in .webp. If the bytes are not a RIFF/WEBP container, the decoder was never the problem, and you can stop reading here and go fix the Accept header.

What can actually decode it

Three honest options, in the order you should consider them:

  • Serve a format Unity already reads. If you control the endpoint, this is the cheapest correct fix and it is not a cop-out: re-encode to PNG or JPG at build time, or content-negotiate on the User-Agent. Zero new code, zero new dependencies, and you keep WebP for the browser clients that benefit from it. If you are loading a handful of images from your own server, do this and stop.
  • A native plugin around libwebp. Several open-source Unity bindings exist and they decode correctly and fast. The cost is not the code, it is the binaries: a .dll, .so, .a and .aar to ship, sign and keep matched to each platform you target — and getting one onto WebGL means an Emscripten build of the library, not the desktop binary you already have.
  • A managed C# decoder. No binaries at all, so the same code compiles for every target Unity does, WebGL included. The trade is CPU: managed decode is slower than libwebp, which matters if you are decoding dozens of images per frame and not at all if you are loading avatars.

Whichever you pick, the shape is the same: bytes in, Color32[] or a pixel buffer out, uploaded into a Texture2D. Do the decode on a worker thread and the upload on the main thread — Texture2D construction and Apply are main-thread-only, and everything before them is not. WebGL is the exception, because it is single-threaded: there the decode runs on the main thread whatever you do, so a large image stalls the frame.

Cache the bytes, not the texture

The second load of the same URL should not hit the network, and almost every implementation of this caches the wrong thing.

WebpByteCache.cs
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;

/// <summary>
/// Cache the DOWNLOADED BYTES, not the decoded Texture2D.
///
/// A texture is expensive to hold (width x height x 4 bytes of GPU memory,
/// per image, forever) and cheap to rebuild. The bytes are the opposite.
/// </summary>
public static class WebpByteCache
{
    static string PathFor(string url)
    {
        using (var sha = SHA256.Create())
        {
            byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(url));
            var name = new StringBuilder(hash.Length * 2);
            foreach (byte b in hash)
            {
                name.Append(b.ToString("x2"));
            }
            // temporaryCachePath is the platform's "you may delete this" location.
            return Path.Combine(Application.temporaryCachePath, name + ".webp");
        }
    }

    public static bool TryRead(string url, out byte[] bytes)
    {
        string path = PathFor(url);
        if (File.Exists(path))
        {
            bytes = File.ReadAllBytes(path);
            return true;
        }
        bytes = null;
        return false;
    }

    public static void Write(string url, byte[] bytes)
    {
        // WebGL has no System.IO filesystem — keep the memory half only there.
#if !UNITY_WEBGL || UNITY_EDITOR
        File.WriteAllBytes(PathFor(url), bytes);
#endif
    }
}

A 2048x2048 texture is 16 MB of GPU memory. The bytes behind it are usually under 200 KB.

Hold the compressed bytes and rebuild the texture on demand: they are one to two orders of magnitude smaller, they survive a scene load, and on desktop and mobile they survive a restart. Holding decoded textures instead is how an image-heavy app ends up with a memory graph that only goes up.

Also called

load webp from url, Texture2D.LoadImage webp blank, UnityWebRequestTexture webp not working, display webp image at runtime, unity webp texture, download webp and convert to sprite — all the same wall, which is that Unity's built-in decoders read PNG, JPG and EXR and nothing else.

WebP for Unity

WebP from a URL on every platform Unity builds for, WebGL included.

WebP for Unity is a complete WebP codec written in C#: it decodes still and animated .webp and encodes it too, with no native plugins, so the same assembly runs on every platform Unity builds for — WebGL included. Loading from a URL, with a memory and disk byte cache, is built in. It is made for the files you do not control: user uploads, third-party APIs and pipelines that are already WebP.

A Unity sample scene titled “WebP from a URL”: a decoded 512 × 512 image above the status line “Loaded 512×512 in 86 ms (MemoryAndDisk)”, a URL field and a Load button
The package’s load-from-URL sample: a .webp requested through UnityWebRequest, decoded and on screen in 86 ms, its bytes held in the memory-and-disk cache so a reload is a cache hit.