仮想と物理とエトセトラ

xRや物理とかごった煮の備忘録的技術ブログ

Unityで外部のzipファイルから複数枚画像を取り出す

今回は小技です。
※物理枠の動作確認がうまくいっていないので、こちらでお茶を濁す

外部ファイルとして、zip中の画像をバイト列で取り出し、テクスチャとして取得します。

準備

今回は、zipファイル中の画像をバイト列のリストとして取り出すクラス(ZipImageLoader.cs)と、それを使用するクラス(ChangeImageByZip.cs)に分けます。

  • ZipImageLoader.cs
クリックで展開
using System.Collections;
using System.Collections.Generic;
using System.IO.Compression;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
/// <summary>
/// ZIPファイルから複数画像を取り出すクラス
/// </summary>
public class ZipImageLoader : MonoBehaviour
{
    /// <summary>
    /// バイト列
    /// </summary>
    private List<byte[]> zipBytes = new List<byte[]>();
    
    /// <summary>
    /// 読み込み完了フラグ
    /// </summary>
    public bool isLoadFinish = false;


    // Start is called before the first frame update
    public ZipImageLoader(string zipPath)
    {
        // 非同期で実行
        Task.Run(() =>
        {
            // zipファイルからzip アーカイブ形式の圧縮ファイルのパッケージを取得
            using (ZipArchive archive = ZipFile.OpenRead(zipPath))
            {
                // 圧縮ファイルパッケージから1ファイル分取り出し
                foreach (ZipArchiveEntry entry in archive.Entries)
                {
                    // ファイルストリームを開く
                    using (Stream stream = entry.Open())
                    {
                        using (var ms = new MemoryStream())
                        {
                            // ファイルストリームをメモリに展開する
                            stream.CopyTo(ms);

                            // メモリの内容をバイト列として格納
                            zipBytes.Add(ms.ToArray());
                            Debug.Log("Loaded No:" + zipBytes.Count);
                        }
                    }
                }
            }
        });

        // 読み込み完了フラグを立てる
        isLoadFinish = true;
    }

    /// <summary>
    /// 画像数取得メソッド
    /// </summary>
    /// <returns></returns>
    public int GetImageNum()
    {
        if(isLoadFinish)
        {
            return this.zipBytes.Count;
        }
        else
        {
            return -1;
        }
    }

    /// <summary>
    /// 任意のindexの画像のバイト列を取得
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public byte[] GetImage(int index)
    {
        if(!isLoadFinish)
        {
            // 読み込み未完了の場合はnullを返す
            return null;
        }
        
        if(index >= 0 && this.zipBytes.Count > index)
        {
            return this.zipBytes[index];
        }

        return null;

    }

    /// <summary>
    /// 画像の全バイト列を取得
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public List<byte[]> GetAllImages()
    {
        if (!isLoadFinish)
        {
            // 読み込み未完了の場合はnullを返す
            return null;
        }
        return this.zipBytes;

    }
}

  • ChangeImageByZip.cs
クリックで展開
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class ChangeImageByZip : MonoBehaviour
{
    /// <summary>
    /// zipファイル名
    /// </summary>
    [SerializeField]
    private string zipFileName;

    /// <summary>
    /// zipファイルパス
    /// </summary>
    private string zipFilePath;

    /// <summary>
    /// zipファイル画像ローダ
    /// </summary>
    private ZipImageLoader zipImageLoader;

    /// <summary>
    /// 画像枚数
    /// </summary>
    private int imageNum = -1;

    /// <summary>
    /// テクスチャリスト
    /// </summary>
    private List<Texture2D> textures;

    /// <summary>
    /// 画像index
    /// </summary>
    private int index = 0;

    /// <summary>
    /// 画像の表示先
    /// </summary>
    [SerializeField]
    private SpriteRenderer spriteRenderer;

    // Start is called before the first frame update
    void Start()
    {
        // zipファイルパスを取得
        GetZipFilePath();

        // zipデータ取得クラスのインスタンスを作成
        zipImageLoader = new ZipImageLoader(zipFilePath);

        // テクスチャ格納リストを定義
        textures = new List<Texture2D>();
    }

    // Update is called once per frame
    void Update()
    {
        CheckUsableImages();



    }

    /// <summary>
    /// 画像が使用可能か確認し、格納する
    /// </summary>
    void CheckUsableImages()
    {
        if (imageNum == -1 && zipImageLoader.isLoadFinish)
        {
            imageNum = zipImageLoader.GetImageNum();

            foreach(var bytes in zipImageLoader.GetAllImages())
            {
                Texture2D texture = new Texture2D(2, 2);
                texture.LoadImage(bytes);

                textures.Add(texture);
            }

            SetTexture(0);
        }
    }

    /// <summary>
    /// テクスチャを反映する
    /// </summary>
    /// <param name="targetIndex"></param>
    void SetTexture(int targetIndex)
    {
        Debug.Log(textures.Count);
        if(textures.Count > targetIndex && targetIndex >= 0)
        {
            Sprite sprite = Sprite.Create(textures[targetIndex], new Rect(0, 0, textures[targetIndex].width, textures[targetIndex].height), new Vector2(0.5f, 0.5f));
            spriteRenderer.sprite = sprite;
            index = targetIndex;
        }
        
    }

    /// <summary>
    /// トグル式にテクスチャを変更する
    /// </summary>
    public void ChangeTexture()
    {
        var targetIndex = index + 1;
        if(targetIndex >= textures.Count)
        {
            targetIndex = 0;
        }
        SetTexture(targetIndex);

    }

    /// <summary>
    /// zipファイルのパスを取得する
    /// </summary>
    void GetZipFilePath()
    {
        string dirPath;
#if WINDOWS_UWP
        // HoloLens上での動作の場合、LocalAppData/AppName/LocalStateフォルダを参照する
        dirPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#else
        // Unity上での動作の場合、Assets/StreamingAssetsフォルダを参照する
        dirPath = UnityEngine.Application.streamingAssetsPath;
#endif
        zipFilePath = Path.Combine(dirPath, zipFileName);

    }

    /// <summary>
    /// ゲームオブジェクト消滅時にTextureの後処理を行う
    /// </summary>
    private void OnDestroy()
    {
        foreach(var texture in textures)
        {
            // 生成したTexture2Dを削除
            Destroy(texture);
        }
    }
}

作成したChangeImageByZip.csをMRTKのボタンにアタッチします。
f:id:napo909:20210529185604p:plain

ボタンで使用するため、ButtonConfigHelperのOnClickにChangeImageByZip.csのChangeTexture()を設定します。
f:id:napo909:20210529175510p:plain

なお、ZipImageLoader.csについて、ビルドターゲットがUWPの場合は下記コンパイルエラーが出力されます。

Assets\ZipImageLoader.cs(30,20): error CS1069: The type name 'ZipArchive' could not be found in the namespace 'System.IO.Compression'. This type has been forwarded to assembly 'System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=■■■■■' Consider adding a reference to that assembly.
Assets\ZipImageLoader.cs(30,41): error CS0103: The name 'ZipFile' does not exist in the current context
Assets\ZipImageLoader.cs(33,26): error CS1069: The type name 'ZipArchiveEntry' could not be found in the namespace 'System.IO.Compression'. This type has been forwarded to assembly 'System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=■■■■■' Consider adding a reference to that assembly.

Unityが正しくライブラリ'System.IO.Compression'を認識できていないようです。
また、ZipFileもエラーが出力されているので、System.IO.Compression.FileSystemも足りないようです。 そのため、下記を参考にAsset配下にcsc.rspファイルを作成します。

docs.unity3d.com

クリックで展開
-r:System.IO.Compression.dll
-r:System.IO.Compression.FileSystem.dll

これでコンパイルエラーが無くなりました。

動作確認

今回は下記画像をzipファイルにし、読み込ませました。
f:id:napo909:20210529180933p:plain

作成したzipファイルをUnity上で実行する場合は/Assets/StreamingAssets/、Hololens上で実行する場合は/LocalAppData/[アプリ名]/LocalState/に配置します。

UnityEditor上で動作させた結果は以下です。
ボタンを押すたびに、zipファイルに格納した画像がトグル式に切り替わり表示されます。
f:id:napo909:20210529185910g:plain

Hololens2でも動作確認済みです。
ファイル別に読めるので画像だけでなく、容量の3Dモデルをまとめておくのにも役立つ気がします。

※MaterialのTextureを動的に切り替えるものを最初作成しましたが、動的に切り替わったり切り替わらなかったりして安定せず。。。
Inspector上で表示してれば表示されるようだけれど、なぜ表示しないとだめなのか定かじゃないです。。。

参考

ファイルの外部読み込み:

bluebirdofoz.hatenablog.com

zipファイルからの読み込み:

stackoverflow.com