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サイトを見ていただくとして、抜粋すると以下のような特徴があります。
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の部分でバージョンを指定することができます。
使ってみた
今回エンコードとデコードを試してみた手順は以下の通りです。
撮影用のCameraを用意し、RenderTextureに対してレンダリングする
1で用意したRenderTextureをWebP形式でエンコードする
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形式にすることでどれくらい容量の削減ができているのかみていきます。
クオリティ80ぐらいでは劣化はほとんど感じません。
画像サイズはというと、、、
PNG:214KB
WebP:42KB
約5分の1のサイズまで削減されました!!
正直二度見しましたが見間違いではないようです。クオリティによるサイズの違いもまとめてみました。
クオリティ80前後が品質的にも画像サイズ的にもちょうどいい感じがしますね。
気になるところ
EncodeToPNGと比較してエンコードに時間がかかる
エンコードにかかる時間を検証してみたところ、EncodeToWebPの方が時間がかかっていることがわかりました。
大体2倍の時間が掛かっていることがわかりました。クオリティ設定によってはエンコードに時間がかかるという内容の記述を見たので、クオリティの違いによる比較も行いましたが、大きな差はありませんでした。
まとめ
今回UnityでもWebPを扱ってみましたが、画像サイズの削減にはかなり効果的な印象でした。エンコード時間が伸びているのは気になりますが、通信量の削減やストレージにも優しいはずなのでメリットも大きく感じました。
REALITY では一緒に開発してくれるメンバーを募集しています。 カジュアル面談も受け付けていますので、お気軽にお申し込みください!