LoginSignup
10
7

More than 3 years have passed since last update.

UnityのNativeArrayを使ってASTCをロードする

Last updated at Posted at 2020-12-14

まえおき

ASTC対応GPUが増えてきた関係でモバイルでは最早必須レベルとなったASTCフォーマット。
またNativeArrayもUnity2018以降から様々な低レベルAPIが追加され、独自処理にも活用しやすくなりました。
今回はAB化せずNativeArray経由でASTCフォーマットをロードする方法を紹介したいと思います。

NativeArrayを使ってASTCをロードするメリット

  • UI等で使う1枚絵といったケースでわざわざAB化しなくても良い(ビルド時間削減
  • ABにしない関係でASTCエンコードにUnityが不要になる(パイプラインに組み込みやすい
  • png/jpgならAB化せずともTexture2D.LoadImageでロードできるが、メモリをマネージド領域にすべて展開する必要があり、メモリ負荷が高い
  • png/jpgのデコードはCPU側になるためCPU負荷も高い

前準備

下調べ

今回はABではないためUnityのASTCエンコードには頼らず、自前でASTCエンコードを行います。
幸いにもARMから公式のエンコーダが公開されてますので、今回はそれを使います。

今回使うASTCエンコーダー
https://github.com/ARM-software/astc-encoder

./astcenc -c sample.png sample.astc 6x6 -medium

こんな感じでお手軽にエンコードできる。Unityも不要なのでciツールとしても導入しやすいと思います。

またエンコードしたデータの展開先はNativeArrayですが、GPUにアップロードするにはTexture2D.LoadRawTextureDataを使います。
ただこのTexture2D.LoadRawTextureDataTexture2D.LoadImageと違い明示的にフォーマットと画像のサイズを指定する必要があります。
ただ、ここで画像メタ情報を別途提供するのはナンセンスなのでASTCフォーマットに含まれるメタ情報からASTCのブロックサイズと画像サイズを取得します。

ASTCのフォーマットを読む

調べたところASTCは以下のようなフォーマットになっているようです。

格納バイト 格納情報
0~3 byte マジックナンバー 0x5CA1AB13
4 byte xのブロックサイズ。(6x6)の場合は0x6
5 byte yのブロックサイズ。(6x6)の場合は0x6
6 byte zのブロックサイズ。2D画像では常にここは0x1
7~9 byte 画像のxサイズ。(1920x1080)なら0x000780
10~12 byte 画像のyサイズ。(1920x1080)なら0x000438
13~15 byte 画像のzサイズ。2D画像なら常にここは0x000001

なので1920x1080画像のブロックサイズ6x6でASTCにエンコードするとヘッダは以下のようになります
(バイトオーダーはリトルエンディアン
13ABA15C 06060180 07003804 00010000
zに関する情報はちょっとわからなかったですが、基本的に1になるようなのであまり気にしなくて良いようです。

このメタ情報を使ってTexture2D.LoadRawTextureDataでロードします

実装


using System;
using System.IO;
using System.Linq;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.IO.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.UI;

public unsafe class AsyncLoader : MonoBehaviour
{
    const int AstcBlockInfoSize = 3;
    const int AstcFullHeaderSize = 16;
    static readonly byte[] AstcHeaderMagicNumber = { 0x5C, 0xA1, 0xAB, 0x13 };

    NativeArray<ReadCommand> commands;
    ReadHandle readHandle;
    Action callback;

    void Start()
    {
        // astcファイルを生成した際のコマンド
        // ./astcenc -c sample.png sample.astc 6x6 -medium
        var filePath = Path.Combine(Application.dataPath, "StreamingAssets/Sign_Okay.astc");
        var fileSize = new FileInfo(filePath).Length;
        commands = new NativeArray<ReadCommand>(1, Allocator.Persistent)
        {
            [0] = new ReadCommand
            {
                Offset = 0,
                Size = fileSize,
                Buffer = (byte*)UnsafeUtility.Malloc(fileSize, UnsafeUtility.AlignOf<byte>(), Allocator.Persistent),
            },
        };
        readHandle = AsyncReadManager.Read(filePath, (ReadCommand*)commands.GetUnsafePtr(), 1);
        callback = () =>
        {
            var buffer = (byte*)commands[0].Buffer;
            if (!IsValid(buffer))
            {
                Dispose();
                throw new NotSupportedException("ASTCファイル内のヘッダが正しくありません");
            }

            var (width, height) = GetTexSize(buffer);
            buffer += AstcFullHeaderSize;
            var textureDataSize = fileSize - AstcFullHeaderSize;
            var tex = new Texture2D(width, height, TextureFormat.ASTC_6x6, false);
            tex.LoadRawTextureData((IntPtr)buffer, (int)textureDataSize);
            tex.Apply();

            GetComponent<RawImage>().texture = tex;
            Dispose();
        };
    }

    void Update()
    {
        if (readHandle.IsValid() && readHandle.Status == ReadStatus.Complete)
        {
            callback?.Invoke();
            callback = null;
        }
    }

    void OnDestroy()
    {
        Dispose();
    }

    static bool IsValid(byte* buffer)
    {
        var isAstc = true;
        var magicNumber =  BitConverter.IsLittleEndian ? AstcHeaderMagicNumber.Reverse() : AstcHeaderMagicNumber;
        foreach (var b in magicNumber)
        {
            isAstc &= *buffer == b;
            if (!isAstc)
            {
                return false;
            }

            buffer++;
        }
        return true;
    }

    static (int Width, int Height) GetTexSize(byte* buffer)
    {
        buffer += AstcHeaderMagicNumber.Length + AstcBlockInfoSize;
        if (BitConverter.IsLittleEndian)
        {
            var width = *buffer++ | *buffer++ << 8 | *buffer++ << 16;
            var height = *buffer++ | *buffer++ << 8 | *buffer << 16;
            return (width, height);
        }
        else
        {
            var width = *buffer++ << 16 | *buffer++ << 8 | *buffer++;
            var height = *buffer++ << 16 | *buffer++ << 8 | *buffer;
            return (width, height);
        }
    }

    void Dispose()
    {
        readHandle.Dispose();
        UnsafeUtility.Free(commands[0].Buffer, Allocator.Persistent);
        commands.Dispose();
    }
}

今回は手抜き工事でASTC_6x6フォーマットでハードコーディングしていますが
もちろんメタ情報を使ってブロックサイズを取得することで動的にフォーマットを指定することも可能です。

結果

実装ポイント

実装ポイントとしては2点です

NativeArrayの解放を必ず忘れない

これはNativrArrayを使う実装すべてにいえることですが、必ず解放を忘れないようにしましょう。
解放を忘れるとリークしてクラッシュします。

LoadRawTextureDataではASTCヘッダ部のメタ情報は不要

ここはハマったポイントなのですがLoadRawTextureDataを使う場合ASTCのヘッダ情報は不要ですのでヘッダの16バイト分を除いてLoadRawTextureDataに渡すようにしましょう。
ヘッダを除かずに渡した場合はヘッダ情報もボディと解釈され、その部分にノイズが表示されてしまいます。

注意事項

PCではASTCに対応してないGPUが多いです。
そのためUnityでもStandalone環境下ではASTCはCPUデコードとなっていますので、PC向けでは使うメリットがないこととパフォーマンス計測時にはご注意ください。

最後に

個人的には依存関係がないかつ2Dテクスチャについてはこの方法を使うのは効果的なのでオススメです。
対応端末がASTC対応端末に限定されてしまうのがデメリットですが、かなり古い端末をサポートしない限りはASTCに対応しているので、新規プロジェクトで導入する分にはまず問題にならないかと思います。

またNativeArrayがこういった低レベルAPIを提供してくれるのは嬉しい限りですね。
DOTSも近いうちにアニメーション対応がなされ、いよいよ現場での利用が現実的になってきそうなのでメモリレイアウトとかちゃんと考えていきたい!

参考

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7