見出し画像

Now in REALITY Tech #37 UnityでWebPを扱えるようにした話

今週の「Now in REALITY Tech」は、WebPをUnityで扱えるようにした話をUnityチームからお送りします。

最近実装されたアバタースタンプでは、生成したスタンプ画像をWebP形式でサーバーにアップロードしています。

もともとREALITYとしては画像表示の一部をWebPとして扱っていましたが、UnityはWebPをサポートしていないため、Unityが絡む部分ではPNG形式で画像を扱っていました。今回UnityでもWebPを扱えるようにしたので、導入方法や気になる点をまとめてみました。

以前WebP形式に対応した時の記事も投稿されているので興味のある方は読んでみてください!


WebPとは

先ほど紹介した記事にもWebP形式についての説明は書いてありますが、こちらの記事でも簡単におさらいしましょう。

WebPとはGoogleが開発しているオープンな画像形式です。
 詳細は、Google公式のWebPサイトを見ていただくとして、抜粋すると以下のような特徴があります。

・非可逆と可逆の両方の方式を扱える。
・非可逆モードでは、JPEG/PNGに比べて2〜3割の画像サイズの軽減。
・画像劣化も少ない。


Unityで扱うにはどうすればいいの?

UnityではWebP形式はサポートされていないので、今回はこちらのライブラリを使用して対応しています。

導入

導入はunitypackageかUPMを使用します。

UPMを使用する場合、プロジェクトフォルダ>Packages>manifest.jsonにこの1行を追加する事でプロジェクトにインポートされます。

"com.netpyoung.webp": "https://github.com/netpyoung/unity.webp.git?path=unity_project/Assets/unity.webp#0.3.4"

unity.webp#0.3.4の部分でバージョンを指定することができます。


使ってみた

今回エンコードとデコードを試してみた手順は以下の通りです。

  1. 撮影用のCameraを用意し、RenderTextureに対してレンダリングする

  2. 1で用意したRenderTextureをWebP形式でエンコードする

  3. 2でエンコードした結果のバイト列をデコードしてTexture2Dとして変換してUnity上で表示

コード

private void WebPTest()
{ 
    // 撮影用カメラの座標を合わせるために取得
    var mainCamera = Camera.main;
    if (mainCamera == null) return;
          
    // 撮影用カメラの座標をMainCameraに揃える
    camera.transform.position = mainCamera.transform.position;
          
    // RenderTextureにCameraの情報をレンダリングする
    const int width = 512;
    const int height = 512;
    var rt = new RenderTexture(width, height, 32, RenderTextureFormat.ARGB32);
    var texture = new Texture2D(width, height);
          
    camera.targetTexture = rt;
    camera.Render();
    camera.targetTexture = null;
          
    RenderTexture.active = rt;

    texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
    texture.Apply();

    RenderTexture.active = null;
    
    rt.Release();
          
    // Texture2DをWebP形式にエンコードする
    // 引数はクオリティ
    var webpBytes = texture.EncodeToWebP(80, out var encodeError);
    if (encodeError != Error.Success) return;
          
    // エンコード結果のバイト列からTexture2Dを生成
    var newTexture = Texture2DExt.CreateTexture2DFromWebP(webpBytes, false, false, out var decodeError);
    if (decodeError != Error.Success) return;

    rawImage.texture = newTexture;
          
    // 比較用にPNG形式でもエンコードしてみる
    var pngBytes = texture.EncodeToPNG();
    var newTexture2 = new Texture2D(width, height);
    newTexture2.LoadImage(pngBytes);

    rawImage2.texture = newTexture2;
}
実行結果

実機結果を見てみると分かるとおり上下反転しています。WebPへのエンコード時に反転してしまっていました。これはライブラリ側でもIssueとして上がっていて自分で反転させて対応するしかない様子。

ということでShaderで上下反転してBlitする作戦にします。

private void WebPTest()
{ 
    // 撮影用カメラの座標を合わせるために取得
    var mainCamera = Camera.main;
    if (mainCamera == null) return;
          
    // 撮影用カメラの座標をMainCameraに揃える
    camera.transform.position = mainCamera.transform.position;
          
    // Y方向反転用のマテリアルをロードする
    var flipVerticalMaterial = Resources.Load<Material>(MaterialPath);
          
    // RenderTextureにCameraの情報をレンダリングする
    const int width = 512;
    const int height = 512;
    var rt = new RenderTexture(width, height, 32, RenderTextureFormat.ARGB32);
    var webpRt = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
    var webpTexture = new Texture2D(width, height);
    var pngRt = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
    var pngTexture = new Texture2D(width, height);
          
    camera.targetTexture = rt;
    camera.Render();
    camera.targetTexture = null;
          
    // WebP用に上下反転Shaderを使って反転させる
    Graphics.Blit(rt, webpRt, flipVerticalMaterial);
    webpTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
    webpTexture.Apply();
          
    // PNGはそのまま
    Graphics.Blit(rt, pngRt);
    pngTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
    pngTexture.Apply();
          
    RenderTexture.active = null;
    rt.Release();
    webpRt.Release();
    pngRt.Release();
          
    // Texture2DをWebP形式にエンコードする
    // 引数はクオリティ
    var webpBytes = webpTexture.EncodeToWebP(80, out var encodeError);
    if (encodeError != Error.Success) return;
          
    // エンコード結果のバイト列からTexture2Dを生成
    var newTexture = Texture2DExt.CreateTexture2DFromWebP(webpBytes, false, false, out var decodeError);
    if (decodeError != Error.Success) return;
    rawImage.texture = newTexture;
          
    // 比較用にPNG形式でもエンコードしてみる
    var pngBytes = pngTexture.EncodeToPNG();
    var newTexture2 = new Texture2D(width, height);
    newTexture2.LoadImage(pngBytes);
    rawImage2.texture = newTexture2;
}

簡単ですが反転Shader

Shader "Unlit/TextureFlipVertical"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 uv = i.uv;
                // Y方向反転
                uv.y = 1.0 - uv.y;
                fixed4 col = tex2D(_MainTex, uv);
                return col;
            }
            ENDCG
        }
    }
}
実行結果

ヨシ!

確認する際クオリティは80で試していますが、0~100で設定することができます。
値が小さいほど圧縮率が高く画像が荒くなります。


画像サイズの削減率は?

WebP形式にすることでどれくらい容量の削減ができているのかみていきます。

512 x 512
WebPクオリティ: 80

クオリティ80ぐらいでは劣化はほとんど感じません。
画像サイズはというと、、、

PNG:214KB
WebP:42KB

約5分の1のサイズまで削減されました!!
正直二度見しましたが見間違いではないようです。クオリティによるサイズの違いもまとめてみました。

クオリティによるサイズの違い
100:84KB
80:42KB
50:32KB
30:27KB
0:15KB
 
・クオリティ100の時点で半分以下になっている
・クオリティ80で100から40 ~ 50%の削減
・それ以降は画質の劣化の割にサイズは変わらない

クオリティ80前後が品質的にも画像サイズ的にもちょうどいい感じがしますね。


気になるところ

EncodeToPNGと比較してエンコードに時間がかかる

エンコードにかかる時間を検証してみたところ、EncodeToWebPの方が時間がかかっていることがわかりました。

検証端末:iPhone 11 Pro

検証方法:512 x 512のアルファ付きデータ8種類をそれぞれPNG形式とWebP形式にエンコードして平均時間を測定し比較する

検証結果:
EncodeToPNG:平均 21.875ms
EncodeToWebP:平均 53.25ms

大体2倍の時間が掛かっていることがわかりました。クオリティ設定によってはエンコードに時間がかかるという内容の記述を見たので、クオリティの違いによる比較も行いましたが、大きな差はありませんでした。


まとめ

今回UnityでもWebPを扱ってみましたが、画像サイズの削減にはかなり効果的な印象でした。エンコード時間が伸びているのは気になりますが、通信量の削減やストレージにも優しいはずなのでメリットも大きく感じました。

REALITY では一緒に開発してくれるメンバーを募集しています。 カジュアル面談も受け付けていますので、お気軽にお申し込みください!