Unity でカメラと被写体の間の遮蔽物を非表示にするスクリプトを作ってみた

3D のゲームを作ったら、カメラと被写体の間の壁で前が見えないという問題が起こりました。
これではどう動いていいかすらわからなくなってしまいます。

![被写体が遮蔽物の壁で見えない.gif](https://qiita-image-store.s3.amazonaws.com/0/46088/9943d2ea-4a40-3fd6-c703-8b5af7ba0d5c.gif)

カメラと被写体の間に毎回の Update で Ray をとばして、その間の対象レイヤーのオブジェクトの Renderer を無効にすることで対処しました。

![カメラと被写体の間に遮蔽物があったら一時的に非表示にする.gif](https://qiita-image-store.s3.amazonaws.com/0/46088/bf7db2fc-1b35-0c0c-458a-8d99d354ee6a.gif)
以下のスクリプトコンポーネントを使うことで、上記の gif のように一時的に壁を非表示にして、壁の奥を表示できます。

もっと良い方法もあるとは思いますが、自分のような 3D 初心者でも簡単に使えるやり方として、 Qiita にメモしておこうと思います。

#動作確認した環境

* windows7 64bit (Home Premium)
* Unity 2018.1.0f2 Personal (64bit)

# ソースコード
このソースコードは [github の gist](https://gist.github.com/sakura-crowd/855cf3687bfc970670118efba2d053dc) でも公開しています。

```SCCameraCoverTransparent.cs
///	License
///	
///	NYSL Version 0.9982
///	
///	A. 本ソフトウェアは Everyone'sWare です。このソフトを手にした一人一人が、
///	   ご自分の作ったものを扱うのと同じように、自由に利用することが出来ます。
///	
///	  A-1. フリーウェアです。作者からは使用料等を要求しません。
///	  A-2. 有料無料や媒体の如何を問わず、自由に転載・再配布できます。
///	  A-3. いかなる種類の 改変・他プログラムでの利用 を行っても構いません。
///	  A-4. 変更したものや部分的に使用したものは、あなたのものになります。
///	       公開する場合は、あなたの名前の下で行って下さい。
///	
///	B. このソフトを利用することによって生じた損害等について、作者は
///	   責任を負わないものとします。各自の責任においてご利用下さい。
///	
///	C. 著作者人格権は SakuraCrowd に帰属します。著作権は放棄します。
///	
///	D. 以上の３項は、ソース・実行バイナリの双方に適用されます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;  // Except

/// <summary>
/// カメラと対象との間の遮蔽物(Cover)を透明化します。
/// カメラに付加してください。
/// 透明にする遮蔽物は Renderer コンポーネントを付加している必要があります。
/// </summary>
public class SCCameraCoverTransparent : MonoBehaviour {


    /// <summary>
    /// 被写体を指定してください。
    /// </summary>
    [SerializeField]
    private Transform subject_;

    /// <summary>
    /// 遮蔽物のレイヤー名のリスト。
    /// </summary>
    [SerializeField]
    private List<string> coverLayerNameList_;

    /// <summary>
    /// 遮蔽物とするレイヤーマスク。
    /// </summary>
    private int layerMask_;

    /// <summary>
    /// 今回の Update で検出された遮蔽物の Renderer コンポーネント。
    /// </summary>
    public List<Renderer> rendererHitsList_ = new List<Renderer>();

    /// <summary>
    /// 前回の Update で検出された遮蔽物の Renderer コンポーネント。
    /// 今回の Update で該当しない場合は、遮蔽物ではなくなったので Renderer コンポーネントを有効にする。
    /// </summary>
    public Renderer[] rendererHitsPrevs_;


    // Use this for initialization
    void Start () {
        // 遮蔽物のレイヤーマスクを、レイヤー名のリストから合成する。
        layerMask_ = 0;
        foreach(string _layerName in coverLayerNameList_)
        {
            layerMask_ |= 1 << LayerMask.NameToLayer(_layerName);
        }

	}


	// Update is called once per frame
	void Update () {
        // カメラと被写体を結ぶ ray を作成
        Vector3 _difference = (subject_.transform.position - this.transform.position);
        Vector3 _direction = _difference.normalized;
        Ray _ray = new Ray(this.transform.position, _direction);

        // 前回の結果を退避してから、Raycast して今回の遮蔽物のリストを取得する
        RaycastHit[] _hits = Physics.RaycastAll(_ray, _difference.magnitude, layerMask_);


        rendererHitsPrevs_ = rendererHitsList_.ToArray();
        rendererHitsList_.Clear();
        // 遮蔽物は一時的にすべて描画機能を無効にする。
        foreach (RaycastHit _hit in _hits)
        {
            // 遮蔽物が被写体の場合は例外とする
            if (_hit.collider.gameObject == subject_)
            {
                continue;
            }

            // 遮蔽物の Renderer コンポーネントを無効にする
            Renderer _renderer = _hit.collider.gameObject.GetComponent<Renderer>();
            if (_renderer != null)
            {
                rendererHitsList_.Add(_renderer);
                _renderer.enabled = false;
            }
        }

        // 前回まで対象で、今回対象でなくなったものは、表示を元に戻す。
        foreach (Renderer _renderer in rendererHitsPrevs_.Except<Renderer>(rendererHitsList_))
        {
            // 遮蔽物でなくなった Renderer コンポーネントを有効にする
            if (_renderer != null)
            {
                _renderer.enabled = true;
            }
        }

    }
}
```

主な処理は Update で毎回次のようなことを繰り返します。

1. 自身と被写体の間に Ray をとばして、その間のオブジェクト(Colliderが必要)を取得する。
2. 取得したオブジェクトの中で、指定されたレイヤーのものは遮蔽物としてメンバの遮蔽物リストに加える。
3. 遮蔽物リストは更新する前に複製し、１つ前の遮蔽物リストを作っておく。(Listの代入だと参照渡しになるので配列にして渡している)
4. 遮蔽物のオブジェクトから Renderer コンポーネントが取り出せたら無効にする。
5. １つ前の遮蔽物リストの中で、現在の遮蔽物リストにいないオブジェクト、要は遮蔽物ではなくなったオブジェクトは再び Renderer コンポーネントを有効にする。

ざっくりいうと、邪魔なときだけ一時的に非表示にして、邪魔じゃなくなったらまた表示する感じです。

#使い方
Camera に上記のスクリプトコンポーネントを付加します。
![2018-05-13_150021.png](https://qiita-image-store.s3.amazonaws.com/0/46088/094547fd-fca4-84fa-4540-fee28d92bcc8.png)
そして、次の項目を設定します。

* Subject_ : 被写体(操作しているキャラクタなど) の Transform を設定してください。
* CoverLayerNameList_ : 遮蔽物とするゲームオブジェクトの属するレイヤー名をリストに追加してください。

これで、このコンポーネントを付加したカメラなどのゲームオブジェクトと被写体の間の遮蔽物は一時的に非表示になります。

#注意点
## 地面がまるごと消えちゃう！
たとえば、 Plane を全体の地面としていて、地形を隆起させると、くぼみにはいったときに、地面全体が非表示になります。

![地面がまるごと非表示になると困る.gif](https://qiita-image-store.s3.amazonaws.com/0/46088/a07e5615-66e5-7d81-0ad9-2d37d78bba01.gif)

対策としては、CoverLayerNameList_ に登録したレイヤー名と該当しないレイヤーを設定しましょう。

## 見せたくない壁が表示されちゃう！
フィールドの外に落ちないように、見えない壁を作ることは良くあることだと思います。たぶん。  
このスクリプトコンポーネントは、そんな見せたくない壁も、遮蔽物じゃなくなったから表示できるように戻してしまいます。

![非表示の壁を表示されると困る.gif](https://qiita-image-store.s3.amazonaws.com/0/46088/29e2a301-5187-4eca-f61a-33d2a335f5ec.gif)

対策としては、さきほどと同じく、CoverLayerNameList_ に登録した遮蔽物となるレイヤー以外のレイヤーを割り当てるようにしてください。

#ちなみに
このスクリプトは unityroom 様で行われた [イベント「ProBuilderゲームジャム」](https://unityroom.com/events/379)に参加したときに作りました。

このときの参加作品「とりでの中のp.b.j.a.m」は次のリンクからプレイできます。
矢印キーとスペースだけの操作で、プレイ時間は2~5分です。
よろしければ遊んでみてください。
[とりでの中のp.b.j.a.m | 無料ゲーム投稿サイト unityroom](https://unityroom.com/games/p_b_j_a_m_inthefort)

#オクルージョンカリングという機能が使えそう。。。
2018/07/25追記。 Unity のカメラにはオクルージョンカリングという機能があり、これが使えそうです。
[オクルージョンカリング - Unity マニュアル](https://docs.unity3d.com/ja/current/Manual/OcclusionCulling.html)

#参照サイト

* [Unity プレーヤーオブジェクトが他のオブジェクトに隠れた時でも見えるようにする – LLC DigiFie](http://www.digifie.jp/blog/archives/1538)
* [イベント「ProBuilderゲームジャム」 | 無料ゲーム投稿サイト unityroom](https://unityroom.com/events/379)
* [とりでの中のp.b.j.a.m | 無料ゲーム投稿サイト unityroom](https://unityroom.com/games/p_b_j_a_m_inthefort)
* [オクルージョンカリング - Unity マニュアル](https://docs.unity3d.com/ja/current/Manual/OcclusionCulling.html)