2025-09-25

[Unity] 在 Unity 使用 webp

最近討論到資源載入的議題,希望減少載入時間.

webp 是一種圖片格式,在相同品質圖片的通常情況下檔案都會較png小.
因此想將此優點導入Unity, 但Unity內的圖片打包會需要使用Unity的資源格式, 所以將目標放在外部載入的圖片.
連結 : GitHub

※原本的專案會使用外部package依賴,這邊我改成使用Unity自己的,比較方便大家使用.

首先, 先將路徑目錄 unity_project\Assets\unity.webp 複製到自己的專案的Plugins (可依自己偏好調整)
※原作者的專案設定為Unity6, 所以我還沒直接開啟過,可能有不確定的問題.

這裡要確認一下你專案中的Libs設定是否都有正確設定 (因為不同Unity版本可能會導致設定遺失)

以下為參考使用範例
/// <summary> 轉成Image用sprite </summary>
public static Sprite CreateSpiteFromByteArray(byte[] bytes)
{
    Texture2D texture;
    if (IsWebP(bytes))
    {
        // test https://developers.google.com/static/speed/webp/images/webplogo.webp?hl=zh-tw
        texture = WebP.Texture2DExt.CreateTexture2DFromWebP(bytes, lMipmaps: false, lLinear: false, out var lError);
    }
    else
    {
        texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
        texture.LoadImage(bytes);
    }
    Sprite spr = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2());

    return spr;
}
/// <summary> 判斷是否為 WebP 格式 </summary>
public static bool IsWebP(byte[] imageData)
{
    return imageData.Length > 11 &&
            imageData[0] == 0x52 && // 'R'
            imageData[1] == 0x49 && // 'I'
            imageData[2] == 0x46 && // 'F'
            imageData[3] == 0x46 && // 'F'
            imageData[8] == 0x57 && // 'W'
            imageData[9] == 0x45 && // 'E'
            imageData[10] == 0x42 && // 'B'
            imageData[11] == 0x50;   // 'P'

    /*if (imageData.Length < 12)
    {
        return false;
    }
    // WebP檔案頭的標識位元組序列
    byte[] webpHeader = { 0x52, 0x49, 0x46, 0x46 };//R對應的ASCII碼用16進製表示為 0x52 ,當然也可以使用十進制 82,或者二進制 0b1010010 (使用二進制需要使用0b前綴來標識)

    // 檢查前4個位元組是否匹配WebP頭部
    for (int i = 0; i < webpHeader.Length; i++)
    {
        if (imageData.Length <= i || imageData[i] != webpHeader[i])
        {
            return false;
        }
    }

    // 提取檔案大小(little-endian)
    int fileSize = BitConverter.ToInt32(imageData, 4);//fileSize+8==imageData.Length

    // 檔案類型識別碼 "WEBP"
    byte[] webpIdentifier = { 0x57, 0x45, 0x42, 0x50 };

    // 檢查檔案類型識別碼
    for (int i = 8; i < 12; i++)
    {
        if (imageData.Length <= i || imageData[i] != webpIdentifier[i - 8])
        {
            return false;
        }
    }
    return true;*/
}

參考資料
netpyoung/unity.webp
An image format for the Web
WebP Container Specification
Unity中使用WebP类型文件



進階篇 WebP Animation

使用相同連結 : GitHub

在使用 WebP.Texture2DExt.CreateTexture2DFromWebP 讀取帶有動畫的WebP時會跳錯
Exception: Failed WebPDecode with error VP8_STATUS_UNSUPPORTED_FEATURE
這時可以使用動畫的解析方法.

因為原作者目前的動畫處理有問題, 所以我另外寫了動畫處理 Texture2DAnimationEx.cs

首先, 建議這邊先當作都是靜態圖處理,讓顯示可以先快速反應
public static Sprite CreateSpiteFromByteArray(byte[] bytes)
{
    try
    {
        Texture2D texture;
        if (IsWebP(bytes))
            if(IsAnimatedWebP(bytes))
                texture = WebP.Texture2DAnimationEx.LoadAnimationOneFrameFromWebP(bytes);
            else
                texture = WebP.Texture2DExt.CreateTexture2DFromWebP(bytes, lMipmaps: false, lLinear: false, out var lError);
        else
        {
            texture = new Texture2D(2, 2, TextureFormat.RGBA32, false, linear: true);
            texture.LoadImage(bytes, markNonReadable: false);
        }
        Sprite spr = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2());

        return spr;
    }
    catch (Exception err)
    {
        Debug.LogException(err);
        return null;
    }
}

public static bool IsAnimatedWebP(byte[] bytes)
{
    if (bytes == null || bytes.Length < 12)
        return false;
    // 檢查 WebP 檔頭 (前 12 bytes
    using (var ms = new System.IO.MemoryStream(bytes))
    using (var br = new System.IO.BinaryReader(ms))
    {
        // 檢查是否有足夠的資料來讀 RIFF header
        if (bytes.Length < 12)
            return false;

        ms.Seek(12, System.IO.SeekOrigin.Begin); // Skip RIFF + Size + WEBP header

        while (ms.Position + 8 <= ms.Length)
        {
            // 讀 chunk type
            string chunkType = new string(br.ReadChars(4));
            if (ms.Position + 4 > ms.Length)
                break;

            int chunkSize = br.ReadInt32();

            if (chunkType == "VP8X")
            {
                if (ms.Position + 1 > ms.Length)
                    break;

                byte flags = br.ReadByte();
                return (flags & 0b00000010) != 0; // Bit 1: Animation flag
            }
            else if (chunkType == "ANIM")
            {
                return true; // 有 ANIM chunk 就是動畫
            }

            // 移動到下一個 chunk(注意要補齊偶數位元組數)
            long skipSize = chunkSize + (chunkSize % 2); // chunk size 必須是偶數
            ms.Seek(skipSize, System.IO.SeekOrigin.Current);
        }

    }
    return false;
}再來就可另外開始處理動畫製作
WebP.Texture2DAnimationEx.LoadAnimationDataFromWebP(bytes)
這邊建議另開一個Thread來處理轉換以避免卡頓
 (List<(byte[] bytes, int timestamp)> frames, int width, int height) animData = await Task.Run(() => WebP.Texture2DAnimationEx.LoadAnimationDataFromWebP(bytes));
回到 Unity Main Thread 的參考做法
var context = System.Threading.SynchronizationContext.Current;

 (List<(byte[] bytes, int timestamp)> frames, int width, int height) animData = await Task.Run(() => WebP.Texture2DAnimationEx.LoadAnimationDataFromWebP(bytes));

TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
context.Post(x => tcs.SetResult(true), null);
await tcs.Task;
完成後回到Unity Main Thread來繼續建立Texture (這功能必須在主執行續下進行)
var frames = new List<(Texture2D, int)>();
for (int i = 0; i < animData.frames.Count; i++)
{
    var frame = animData.frames[i];
    var tex = new Texture2D(animData.width, animData.height, TextureFormat.RGBA32, false);
    tex.wrapMode = TextureWrapMode.Clamp;
    tex.LoadRawTextureData(frame.bytes);
    tex.Apply(updateMipmaps: false, makeNoLongerReadable: true);
    frames.Add((tex, frame.timestamp));
}
如果需要轉成Sprite也可在這邊進行, 如果轉換會卡頓可以在每個frame解析的迴圈中增加等待
這邊就依據專案偏好自行使用 await 或 yield return

到這邊就完成轉換了.
可以隨意製作簡單的撥放.
※ 原作者的動畫功能目前有問題, 找時間我再丟修正上去試試, 這樣就可以直接使用原本提供的.
WebP.Experiment.Animation.WebPDecoderWrapper.Decode(bytes);
參考資料
WebP Container API 說明文件

沒有留言:

張貼留言