【Unity UGUI】Maskを使わずに実現する「穴あき」ハイライト演出
はじめに
UGUIで部分的にハイライトを当てたい場面、特にチュートリアル中の誘導UIでは「特定のボタンだけを明るく見せ、他の部分を半透明で覆う」演出がよく使われます。
一般的には Mask や RectMask2D を利用しますが、これらにはいくつかの課題があります。
-
Mask:Stencilバッファを使うため、描画コストが高い -
RectMask2D:矩形領域しか扱えず、穴あき表現(逆マスク) はできない
そこで今回は、MaskableGraphic を継承し、完全プログラム制御で「逆マスク(穴あきマスク)」を生成する方法を紹介します。
最終的には、ハイライト部分がクリック可能で、他の部分はブロックされる、高性能な HollowOutMask コンポーネントを実装していきます。
目標仕様
| 機能 | 説明 |
|---|---|
| 視覚効果 | 自身の RectTransform 領域内に半透明のマスクを描画し、指定の RectTransform 部分を「くり抜く」 |
| 射線処理 | 穴の部分はクリックが通過し、マスク部分はクリックをブロック |
| 動的追従 | 対象オブジェクトが動いたりスケールが変化しても、くり抜き領域が追従 |
これを、Mask 系コンポーネントを使わずに、頂点操作だけで実現します。
実装の基本構造
HollowOutMask は MaskableGraphic を継承し、OnPopulateMesh() をオーバーライドします。
頂点生成の考え方
想像してみてください。
マスク全体を表す「外側の矩形(Outer Rect)」と、くり抜く「内側の矩形(Inner Rect)」があるとします。
この2つの矩形を組み合わせ、外側を塗りつつ内側を抜くには、8つの頂点と8枚の三角形が必要です。
コード例:OnPopulateMesh()
protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();
if (_isTargetNull)
{
base.OnPopulateMesh(vh);
return;
}
// Outer Rect(自身のRectTransform基準)
float outerLx = rectTransform.rect.xMin;
float outerBy = rectTransform.rect.yMin;
float outerRx = rectTransform.rect.xMax;
float outerTy = rectTransform.rect.yMax;
vh.AddVert(new Vector3(outerLx, outerTy), color, Vector2.zero);
vh.AddVert(new Vector3(outerRx, outerTy), color, Vector2.zero);
vh.AddVert(new Vector3(outerRx, outerBy), color, Vector2.zero);
vh.AddVert(new Vector3(outerLx, outerBy), color, Vector2.zero);
// Inner Rect(穴部分)
float innerLx = _targetMin.x;
float innerBy = _targetMin.y;
float innerRx = _targetMax.x;
float innerTy = _targetMax.y;
vh.AddVert(new Vector3(innerLx, innerTy), color, Vector2.zero);
vh.AddVert(new Vector3(innerRx, innerTy), color, Vector2.zero);
vh.AddVert(new Vector3(innerRx, innerBy), color, Vector2.zero);
vh.AddVert(new Vector3(innerLx, innerBy), color, Vector2.zero);
// Triangles(外側と内側の間を埋める)
vh.AddTriangle(0, 1, 5); vh.AddTriangle(5, 4, 0);
vh.AddTriangle(1, 2, 6); vh.AddTriangle(6, 5, 1);
vh.AddTriangle(2, 3, 7); vh.AddTriangle(7, 6, 2);
vh.AddTriangle(3, 0, 4); vh.AddTriangle(4, 7, 3);
}
これで、中央が透明な「リング状ポリゴン を生成できます。
ポイントは:
-
すべての頂点を 同一ローカル座標系 で扱うこと
-
外側と内側の矩形を8つの三角形に分割すること
ターゲット位置の動的追跡
HollowOutMask の肝は、ターゲットの位置やサイズが変わったときに、くり抜き部分を自動更新する点です。
LateUpdateによる安定した追従
void LateUpdate()
{
bool selfRectChanged = (_lastSelfRect != rectTransform.rect);
bool targetMatrixChanged = (_lastTargetMatrix != _target.localToWorldMatrix);
if (selfRectChanged || targetMatrixChanged)
{
ForceRefresh();
}
}
なぜ LateUpdate?
UGUIのレイアウト更新(LayoutGroupなど)はUpdate中に行われるため、
LateUpdateで追従することで**「1フレーム遅れ」や「不一致」**を防ぎます。
高効率な変化検知
_target.localToWorldMatrix は、Transformのあらゆる変更(位置・回転・スケール)を反映する行列です。
これをキャッシュ比較することで、最小コストで変化を検知できます。
ForceRefresh:再計算と再描画
private void ForceRefresh()
{
_lastSelfRect = rectTransform.rect;
if (_isTargetNull) return;
_lastTargetMatrix = _target.localToWorldMatrix;
_target.GetWorldCorners(_targetWorldCorners);
Matrix4x4 selfWorldToLocal = rectTransform.worldToLocalMatrix;
Vector3 vMin = new(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 vMax = new(float.MinValue, float.MinValue, float.MinValue);
for (int i = 0; i < 4; i++)
{
Vector3 localPoint = selfWorldToLocal.MultiplyPoint3x4(_targetWorldCorners[i]);
vMin = Vector3.Min(vMin, localPoint);
vMax = Vector3.Max(vMax, localPoint);
}
_targetMin = vMin;
_targetMax = vMax;
SetVerticesDirty();
}
これにより、世界座標 → 自身ローカル座標 の変換を正確に行い、
Canvasに「頂点データ更新が必要」と通知します。
射線処理:ICanvasRaycastFilter の活用
最後に、穴の部分をクリックが通過するように設定します。
public bool IsRaycastLocationValid(Vector2 screenPos, Camera eventCamera)
{
if (!isActiveAndEnabled) return true;
if (_isTargetNull) return true;
// 穴の部分は Raycast 無効(クリックを通過させる)
return !RectTransformUtility.RectangleContainsScreenPoint(_target, screenPos, eventCamera);
}
EventSystem からの「この点は命中したか?」という問いに対して:
-
穴の部分 → 「命中していない」(
false) → 下のボタンまで届く -
マスク部分 → 「命中した」(
true) → クリックをブロック
という仕組みになります。
まとめ
| 要素 | 解説 |
|---|---|
| MaskやRectMask2Dを使わない | パフォーマンス改善と柔軟性確保 |
| 頂点を自前で生成 | 任意形状のマスクを実現 |
| LateUpdate+Matrix比較 | 高速かつ正確な位置追従 |
| ICanvasRaycastFilter | 穴部分のクリック透過を簡潔に実装 |
この方法を使えば、
「特定UIをハイライトしつつ、他を暗く覆う」 演出を、軽量かつ完全制御で実現できます。
Discussion