single.php

Unity でGameObjectの隠れた部分をハイライトにする手順

Unityで、2Dゲームでよく見かける壁とプレイヤーが重なって隠れてしまった部分にハイライトを適用する方法を備忘録的に投稿します。

隠れた部分のハイライト

2Dゲームで、プレイヤーが建物と重なった時に隠れた部分にハイライトが表示される場合を、よく見かけます。

こんな感じで、プレイヤー全体が隠れてしまうと操作しているユーザーが見失ってしまうのを防ぐ親切機能です。

建物の[Sprite Mask]と、プレイヤーの[Mask Interaction]で簡単に実現できそうだったのですが、敵キャラなどにも適用するために汎用化してみると、そんなに甘くない実装だったので、記録として手順を残しておきます。

建物側には SpriteMask が必要

ハイライトを表示する建物などの GameObject には[インスペクター]画面で[Sprite Mask]を追加しておきます。

[Sprite]には[Sprite Renderer]などで適用しているスプライト画像を設定します。

プレイヤー側には工夫が必要

ハイライトする範囲を決めるプレイヤーや敵キャラには、重なっている部分をハイライトする部分のマテリアルを作成する必要があります。

既存のマテリアルでも表示はされますが、建物にプレイヤーの他に敵キャラなど複数の重なりが発生した場合に、不具合が表示されたので C# で動的に生成する工夫が必要でした。

1.[プロジェクト]画面の空いている場所で右クリックして表示されたポップアップメニューで[Create|Shader|URP Unlit Shader]を選択します。

2. 追加された[URP Unlit Shader]の名前を “URP_Ulit_Shader” に変更します。

3.[URP_Ulit_Shader]をダブルクリックして編集して保存します。

Shader "Custom/Silhouette"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Silhouette Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "RenderType" = "Transparent"
            "RenderPipeline" = "UniversalPipeline"
            "CanUseSpriteAtlas" = "True"
        }

        Cull Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            CBUFFER_START(UnityPerMaterial)
                half4 _Color;
            CBUFFER_END

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 color : COLOR;
            };

            Varyings vert(Attributes input)
            {
                Varyings output;

                output.positionHCS =
                    TransformObjectToHClip(input.positionOS.xyz);

                output.uv = input.uv;
                output.color = input.color;

                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                half4 texColor =
                    SAMPLE_TEXTURE2D(
                        _MainTex,
                        sampler_MainTex,
                        input.uv
                    );

                half alpha = texColor.a * input.color.a;

                return half4(_Color.rgb, alpha * _Color.a);
            }

            ENDHLSL
        }
    }
}

4. 追加したシェーダーを利用するためにC#スクリプト[SilhouetteContainer]を追加して、編集します。

using UnityEngine;

public class SilhouetteContainer : MonoBehaviour
{
    [Header("シルエット用シェーダー (URP_ULit_Silhouetteをセット)")]
    [SerializeField] private Shader silhouetteShader;

    [Header("シルエットの色")]
    [SerializeField] private Color silhouetteColor = Color.white;

    [Header("描画の優先順位 (親のOrder + この数値)")]
    [SerializeField] private int orderOffset = 10;

    private SpriteRenderer parentSpriteRenderer;
    private SpriteRenderer childSpriteRenderer;
    private Material silhouetteMaterial;

    private void Awake()
    {
        parentSpriteRenderer = GetComponent<SpriteRenderer>();
        if (parentSpriteRenderer == null) return;

        GameObject childObj = new GameObject("XRayHighlight_AutoGenerated");
        silhouetteMaterial = new Material(silhouetteShader);
        silhouetteMaterial.color = silhouetteColor;

        childObj.transform.SetParent(transform);
        childObj.transform.localPosition = Vector3.zero;
        childObj.transform.localRotation = Quaternion.identity;
        childObj.transform.localScale = Vector3.one;

        childSpriteRenderer = childObj.AddComponent<SpriteRenderer>();
        
        childSpriteRenderer.sprite = parentSpriteRenderer.sprite;
        childSpriteRenderer.material = silhouetteMaterial;
        childSpriteRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;

        childSpriteRenderer.sortingOrder = parentSpriteRenderer.sortingOrder + orderOffset;
    }

    private void LateUpdate()
    {
        if (parentSpriteRenderer != null && childSpriteRenderer != null)
        {
            childSpriteRenderer.sprite = parentSpriteRenderer.sprite;
            childSpriteRenderer.flipX = parentSpriteRenderer.flipX;
            childSpriteRenderer.flipY = parentSpriteRenderer.flipY;
            childSpriteRenderer.transform.localScale = Vector3.one;
            childSpriteRenderer.transform.localRotation = Quaternion.identity;
            childSpriteRenderer.enabled = parentSpriteRenderer.enabled;
        }
    }

    void OnDestroy()
    {
        if (silhouetteMaterial != null)
        {
            Destroy(silhouetteMaterial);
        }
    }
}

5. プレイヤーなどハイライトを表示する GameObjectの[インスペクター]画面も編集した[SilhouetteContainer]スクリプトを追加して[URP_Ulit_Silhouette]シェーダーなどを設定します。

6. ゲームを開始すると[Sprite Mask]を追加した GameObject とプレイヤーが重なって隠れた部分にハイライトが適用されるようになります。

まとめ

今回はUnityで、2Dゲームでよく見かける壁とプレイヤーが重なって隠れてしまった部分にハイライトを適用する方法について書きました。

建物の[Sprite Mask]と、プレイヤーの[Mask Interaction]で1対1であれば、簡単に実現できましたが、敵キャラなどにも適用するために汎用化してみると、ハイライトの形状が定まらない現象が発生したので、シェーダーを自作して汎用化ができました。

Unityで GameObject 同士が重なった時にハイライト表示がしたい人の参考になれば幸いです。

スポンサーリンク

最後までご覧いただき、ありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です