【Unity】uGUIにアルファ付きのマスクを掛ける

uGUIにアルファ付きのマスクを掛ける方法です。

Unity2018.3.1

はじめに

MaskやRectMask2Dを使うとuGUIにマスクをかけられます。

docs.unity3d.com

docs.unity3d.com

ただこのマスクは各ピクセルについて完全にマスクするかしないかのどちらかになり、半透明のマスクはかけられません。
アルファ付きのマスクを掛けるには少し込み入った実装なので、この記事ではその実装方法を紹介します。

本記事では説明の簡易化のため、CanvasのRenderModeがOverlayの場合は想定しません。
(Cameraが存在せず、ビュー空間への変換にひと手間掛かるため)

また、GUIを映すCameraはOrthographicな場合のみを想定します。

アルファ用のRectTransformのビューポート座標を求める

まず、アルファ用テクスチャ設定用のRectTransformのビューポート座標を求めます。
RectTransformのワールド座標はRectTransform.GetWorldCorners()で取得できるので、そこから計算していきます。

f:id:halya_11:20190210021847p:plain

ソースコードはこんな感じです。

// RectTransformのワールド空間におけるサイズと座標を取得
var worldCorners = new Vector3[4];
rectTransform.GetWorldCorners(worldCorners);
var worldCornerMin = worldCorners[0];
var worldCornerMax = worldCorners[2];
var worldSize = worldCornerMax - worldCornerMin;
var worldCenter = worldCornerMin + worldSize * 0.5f;

// RectTransformのスクリーン座標を求める
var screenConerMin = RectTransformUtility.WorldToScreenPoint(camera, worldCornerMin);
var screenConerMax = RectTransformUtility.WorldToScreenPoint(camera, worldCornerMax);

// RectTransformのビューポート座標を求める
var screenSize = new Vector2(Screen.width, Screen.height);
var viewportCornerMin = screenConerMin / screenSize;
var viewportCornerMax = screenConerMax / screenSize;
var viewportRect = new Rect(viewportCornerMin, viewportCornerMax - viewportCornerMin);

RectTransform.GetWorldCorners()でRectTransformの四隅のワールド座標を求め、
それをRectTransformUtility.WorldToScreenPoint()でスクリーン座標に変換します。
あとはスクリーンサイズを利用してビューポート座標を求めるだけです。

ビューポート座標をビュー空間に変換する

次にビューポート座標をビュー空間の座標に変換します。

f:id:halya_11:20190210022942p:plain

ソースコードは次の通りです。

var orthographicSize = camera.orthographicSize;
float aspectRatio = (float)Screen.width / Screen.height;

// ビューポート座標をビュー空間に変換する行列
var viewportToCamera = Matrix4x4.Scale(new Vector3(orthographicSize * aspectRatio, orthographicSize, 1))
    * Matrix4x4.Scale(new Vector3(2, 2, 1))
    * Matrix4x4.Translate(new Vector3(-0.5f, -0.5f, 0));
            
// ビュー空間におけるRectTransformの最小地点と最大地点
var cameraCornerMin = viewportToCamera.MultiplyPoint(viewportCornerMin);
var cameraCornerMax = viewportToCamera.MultiplyPoint(viewportCornerMax);

今回は変換行列を作りました。
まずMatrix4x4.Scale(new Vector3(2, 2, 1)) * Matrix4x4.Translate(new Vector3(-0.5f, -0.5f, 0));でビューポート座標を-1~1の範囲に変換しています。
さらにそれをorthographicSizeaspectRatioによりスケーリングさせることでビュー空間に変換しています。

マスクを画面全体に映すようなVP行列を作る

次にマスクを画面全体に映すようなVP行列を作ります。

f:id:halya_11:20190210024539p:plain

VP行列はビュー空間において映す範囲がわかっていればMatrix4x4.Orthoで簡単に作れます。

// P行列を求める
var matrixVP = Matrix4x4.Ortho
(
    cameraCornerMin.x,
    cameraCornerMax.x,
    cameraCornerMin.y,
    cameraCornerMax.y,
    zNear,
    zFar
);
// プラットフォーム間の差異を吸収
matrixVP = GL.GetGPUProjectionMatrix (matrixVP, false);

// VP行列を求める
matrixVP = matrixVP * camera.worldToCameraMatrix;

GL.GetGPUProjectionMatrix()を使っているのはプラットフォーム間の差異を吸収するためです。
下記を参考にさせていただきました。

tech.drecom.co.jp

各uGUIのシェーダで処理する

あとはこの行列とマスクテクスチャをマスクを掛ける対象のuGUIのシェーダに渡し処理します。

まず頂点シェーダでは先ほどの行列を使って頂点をビュー空間に変換します。
ビュー空間に変換した頂点は-1~-1の範囲を取りますが、今回はこれをuvとして使うので0~1の範囲に変換します。

float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
OUT.alphaMaskUv = mul(_AlphaMaskMatrixVP, worldPos).xy;
OUT.alphaMaskUv = OUT.alphaMaskUv * 0.5 + 0.5;

あとはフラグメントシェーダでこのUVを使ってマスクテクスチャをサンプリングするだけです。

half4 mask = tex2D(_AlphaMaskTex, IN.alphaMaskUv.xy);

すると、対象のピクセルがマスクテクスチャと重なる部分のマスクテクスチャをサンプリングできます。
これを元の色にかけ合わせたらアルファ付きマスクの完成です。

color.a *= mask.a;

関連

light11.hatenadiary.com