💡

【Unity UGUI】Maskを使わずに実現する「穴あき」ハイライト演出

に公開

はじめに

UGUIで部分的にハイライトを当てたい場面、特にチュートリアル中の誘導UIでは「特定のボタンだけを明るく見せ、他の部分を半透明で覆う」演出がよく使われます。
一般的には MaskRectMask2D を利用しますが、これらにはいくつかの課題があります。

  • Mask:Stencilバッファを使うため、描画コストが高い

  • RectMask2D:矩形領域しか扱えず、穴あき表現(逆マスク) はできない

そこで今回は、MaskableGraphic を継承し、完全プログラム制御で「逆マスク(穴あきマスク)」を生成する方法を紹介します。
最終的には、ハイライト部分がクリック可能で、他の部分はブロックされる、高性能な HollowOutMask コンポーネントを実装していきます。


目標仕様

機能 説明
視覚効果 自身の RectTransform 領域内に半透明のマスクを描画し、指定の RectTransform 部分を「くり抜く」
射線処理 穴の部分はクリックが通過し、マスク部分はクリックをブロック
動的追従 対象オブジェクトが動いたりスケールが変化しても、くり抜き領域が追従

これを、Mask 系コンポーネントを使わずに、頂点操作だけで実現します。


実装の基本構造

HollowOutMaskMaskableGraphic を継承し、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