UnityでVR対応のどこでもドア実現を目指す その２ Oblique Near-Plane Clipping編

# 前回までのあらすじ
[前回の記事](http://qiita.com/tsgcpp/items/37fea2ef7e140c397968#%E7%8F%BE%E6%99%82%E7%82%B9%E3%81%AE%E8%87%B4%E5%91%BD%E7%9A%84%E3%81%AA%E6%AC%A0%E7%82%B9) でどこでもドアのワープを実装したら、そこには映ってはイケないものが。。。

![](https://qiita-image-store.s3.amazonaws.com/0/188798/2a809b7a-cf0e-4199-9234-d926d36575b2.jpeg)
![](https://qiita-image-store.s3.amazonaws.com/0/188798/4de8a74c-d0c2-94dd-eb26-b4194f27e211.jpeg)

白いやつ、あんたは映っちゃだめなんですよ。

# 今回やること
ということで今回はこのどこでもドアに後ろにあって、映るべきでない白いやつを映らないようにしたいと思います。

# 実装までの道のり
結論を知りたい人は記事下部の「スクリプトの実装とカメラへの設定」へどうぞ

## Oblique Near-Plane Clipping
ここで登場するのが **Oblique Near-Plane Clipping** というテクニック。
(詳しい解説は後日補足編を記事にします)

大層な名前がついていますが、簡単に言うとカメラのクリップ(Clipping)空間の近接面(Near-Plane)を斜め(Oblique)にしたものを設定するテクニックです。
もっと早い話が以下の青い空間にあるオブジェクトだけ、描画対象にするためのテクニックです。

![](https://qiita-image-store.s3.amazonaws.com/0/188798/a2abaeec-750e-40ad-b64d-686b55039b8f.jpeg)


どうやるのか？
残念ながらエディタのどこを見てもそんな事ができる設定はなさそう。。。
いろいろ探した結果 [Camera.CalculateObliqueMatrix](https://docs.unity3d.com/ScriptReference/Camera.CalculateObliqueMatrix.html) なるメソッドを発見！

clipPlaneを表すVector4を渡すと、それをNear-PlaneとするProjection Matrixを返してくれるらしい。

## Camera.CalculateObliqueMatrixってどうやって使うの？
というかそもそもこのclipPlaneってなんだ？？？
メソッド公開するならもうちょっとドキュメント書いてくれよ。。。と愚痴を述べつつ
色々調べた結果以下の式の $ (a, b, c, d) $ をVector4として渡すようです。

```math
ax + by + cz + d = 0
```

さて、ここで高校数学が登場します(まじかよ。。。

## 平面の方程式の求め方
3次元空間における平面の方程式は、以下の2つの要素があれば求めることが出来ます。

1. 正規化された平面の単位法線ベクトル $ \vec{n} $
2. 平面上の任意の点 $ p $


詳しい解説は検索してもらえればと思います。
答えは $ a, b, c $ は $ \vec{n} $ の各要素、$ d $ は $ -\vec{n} \cdot p $ になります。

## 平面を求めるメソッドのコーディング
上記の法則に従い`CalculateClipPlane`メソッドを実装します。

```csharp
static Vector4 CalculateClipPlane(Vector3 clip_plane_normal, Vector3 clip_plane_arbitary_position) {
  float distance = -Vector3.Dot(clip_plane_normal, clip_plane_arbitary_position);
  Vector4 clip_plane = new Vector4(clip_plane_normal.x, clip_plane_normal.y, clip_plane_normal.z, distance);

  return clip_plane;
}
```

上記のメソッドに平面の単位法線ベクトル、平面上の任意の点を渡せば平面の方程式がVector4で取得できます。


## 平面と垂直な法線と平面上の任意の点
これはちょうどいいものが有ります。Near-Clip PlaneとしてTransformを使いましょう。
transform.forward(zの正の向き)を法線、transfrom.positionを平面上の任意の点としましょう。
xy-平面をそのまま平面として扱うイメージです。

![clip plane image.JPG](https://qiita-image-store.s3.amazonaws.com/0/188798/617979d2-bc66-a66a-38a0-7ff561e30c2e.jpeg)

よし、これで解決！！！　になりません。。。


## カメラ空間のパラメータを求める
実は`Camera.CalculateObliqueMatrix`に渡すパラメータはカメラ空間で渡す必要があるようです。
(この部分も補足編に少し書こうと思います。)

ということで`CalculateClipPlane`に渡すパラメータもカメラ空間に変換しましょう。
`Camera.worldToCameraMatrix`でカメラ空間に変換するmatrixが取得出来ます。
これを使うことでカメラ空間における平面に垂直な法線と平面上の任意の点が求められます。

```csharp
Matrix4x4 world_to_camera_matrix = camera.worldToCameraMatrix;
Vector3 clip_plane_normal = world_to_camera_matrix.MultiplyVector(clip_plane_transform.forward);
Vector3 clip_plane_arbitary_position = world_to_camera_matrix.MultiplyPoint(clip_plane_transform.position);
```

後はこれを`CalculateClipPlane`に渡せばカメラ空間での平面の方程式がVector4で求められます。

# スクリプトの実装とカメラへの設定
ここまでくれば後はUnity向けに実装するだけです。
※カメラ空間におけるClip Planeの位置は、カメラが移動すると変わることに注意してください。

以下はサンプルなのでよろしければ参考にして下さい。

```csharp
using UnityEngine;

[RequireComponent(typeof(Camera))]
public class NearPlaneClip : MonoBehaviour {
  private Camera _camera;

  // Near-Clip Planeとして扱うTransform
  public Transform _clip_plane;

  void Start() {
    // Cameraコンポーネントを取り出す
    _camera = GetComponent<Camera>();
  }

  void LateUpdate() {
    // 毎フレーム Projection Matrix を更新
    Vector4 clip_plane = CalculateClipPlane(_camera, _clip_plane);
    _camera.projectionMatrix = _camera.CalculateObliqueMatrix(clip_plane);
  }

  private static Vector4 CalculateClipPlane(Camera camera, Transform clip_plane_transform) {
    Matrix4x4 world_to_camera_matrix = camera.worldToCameraMatrix;
    Vector3 clip_plane_normal = world_to_camera_matrix.MultiplyVector(clip_plane_transform.forward);
    Vector3 clip_plane_arbitary_position = world_to_camera_matrix.MultiplyPoint(clip_plane_transform.position);

    return CalculateClipPlane(clip_plane_normal, clip_plane_arbitary_position);
  }

  private static Vector4 CalculateClipPlane(Vector3 clip_plane_normal, Vector3 clip_plane_arbitary_position) {
    float distance = -Vector3.Dot(clip_plane_normal, clip_plane_arbitary_position);
    Vector4 clip_plane = new Vector4(clip_plane_normal.x, clip_plane_normal.y, clip_plane_normal.z, distance);

    return clip_plane;
  }
}
```

手順は前回の記事からの続きとして書きます。

1. 赤カメラにアサイン
2. どこでもドアの移動先を想定した場所にNear-Clip Planeとして利用するTransformを設置
3. 上記Transformをスクリプトの`Clip_plane`に設定

これで再生すれば白球が消えると思います！

![result.JPG](https://qiita-image-store.s3.amazonaws.com/0/188798/32f7d907-768d-1284-cb22-2ee526ceb877.jpeg)

# そして、いざVRへ！！！
ぼくはVRがやりたいんだ！！！
ということで、いざVRへ！
ってあれ。。。

![ng vr clip.PNG](https://qiita-image-store.s3.amazonaws.com/0/188798/9dd00c7f-45d9-a103-89ff-9f076d1ff1a1.png)

***なぜ貴様がそこにいる！？***

次回に続く

# 今回のまとめ
今回は予告どおりストーリ調にしてみました(といっても余計なコメント追加してただけのような。。。)
まだまだどこでもドアまでの道のりは長いですが、一つずつ解決していきたいです。

よろしければ記事について、感想や指摘などお願いします。