すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【Unity】鏡に映るは鏡の世界なり (Unityで鏡の実装)

概要

タイトル訳:Unityで鏡を作りたい! 今回紹介する考え方は応用が利くので、何かのお役に立てばと思います。

※Qiitaの記事と全く同じです

動作環境

  • Unity 2019.4.0f1

原理

まず初めに、鏡に映るものをどうやってレンダリングするかを考えましょう。

発想

鏡の映像を撮りたいということで、 カメラに鏡の世界を撮ってきてもらいましょう!

f:id:tsgcpp:20200531212629g:plain
鏡の世界に切り替えるイメージ

※アニメーション中の影の動きは気にしないでください ※青暗い面は鏡の境界面を表現しています。

こちらのイメージ映像の通りに世界を反対にしましょう。 ここで重要なのは「鏡の面を境にして世界を反対にする」ことです。

実装

実現方法

鏡の境界を中心として、鏡面方向のスケールを-1にします。 ということで 射影変換行列 を使いましょう!

変換の順番は

  1. 鏡の位置を原点とする座標系に変換(鏡のローカル座標に変換)
  2. 原点を中心として、Z方向のスケールを-1に変換
  3. 1.の逆変換
  4. 3.の後にできた世界をそのままレンダリング (通常のカメラ座標への変換)

上記変換を実施する行列をカメラのViewMatrixより先に実施されるように差し込んであげれば良いです。

以下はイメージです。表示している軸はワールド座標系の軸です。

f:id:tsgcpp:20200531213341g:plain
鏡を境界に世界を反対にする射影変換のイメージ

この動画の通りカメラの位置や回転は変更する必要はありません。 世界だけ鏡の境界で反対にした後に映像を撮れば鏡の映像になります。

鏡用カメラのCulling Mask

新たにMirrorレイヤーを追加して、鏡用カメラのCulling Maskからは除外します。 鏡オブジェクトのレイヤーをMirrorにして、鏡用カメラにレンダリングされないようにしましょう。

f:id:tsgcpp:20200531213655p:plain
Culling Mask

変換を実施するコード

Unityのベクトルは列オーダーなので、先に実施させたい場合は後側に行列を挿入することになります。

mirrorCamera.worldToCameraMatrix =
    mainCamera.worldToCameraMatrix *  // 通常のカメラ座標への位置 (4. の変換)
    mirror.localToWorldMatrix *       // 3. の変換
    Matrix4x4.Scale(new Vector3(1, 1, -1)) *  // 2. の変換
    mirror.worldToLocalMatrix;        // 1. の変換

鏡用カメラの各種設定

※2020/07/12 追記

  • MainCameraに合わせていくつかの項目を設定
// targetTextureを設定するとprojectionMatrixが更新されてしまうため、
// 対象のカメラのもので上書き
mirrorCamera.projectionMatrix = camera.projectionMatrix;

// 影の描画に使用されるため対象のカメラに合わせる
mirrorCamera.fieldOfView = camera.fieldOfView;
mirrorCamera.aspect = camera.aspect;

ここまでの結果

早速、以下のシーンで鏡用カメラの映像を見てみましょう!

f:id:tsgcpp:20200531213921j:plain
テストシーン

こうなります。。。

f:id:tsgcpp:20200531213947j:plain
テストシーンのNGな結果

何が起こっているというと、モデルのポリゴンの向きが反対になってるんですね。

f:id:tsgcpp:20200531214109g:plain
ポリゴンが反対になっていることの確認

TransformのScaleが-1の場合は、Unityが勝手にポリゴンの向きを逆にしてレンダリングしてくれますが、 カメラのViewMatrixでの反対化はこちらで逆にしてやる必要があります。

Qiitaで以前投稿した記事にて、Unityのスケールとポリゴンの向きについて語っています。

Frontface Cullingに切り替える

以下のメソッドをコールすればBackface CullingからFrontface Cullingに切り替わります。

GL.invertCulling = true;

注意点として、カメラの描画完了後は完了前の状態に戻してあげてください。 MainCameraなど通常のカメラまでポリゴンが反対になります。

bool oldCulling = GL.invertCulling;
GL.invertCulling = true;  // Frontface Cullingに切り替える

mirrorCamera.Render();  // レンダリングの実施

GL.invertCulling = oldCulling;  // 切り替え前に戻す

f:id:tsgcpp:20200531214328j:plain
Frontface Cullingにした結果

問題なく鏡の世界が描画されました。影の向きも鏡の世界のものになっていますね。

鏡の映像を鏡に反映

RenderTextureやDepthMaskを使って鏡に反映してください。 サンプルシーンを記事の最後に載せておきます。

f:id:tsgcpp:20200531214842g:plain
複数の視点から見た結果

違和感なさそうですね。

DepthMask方式が使用可能な理由

※DepthMaskについてはこちらのQiitaの記事をご参照ください。

これは鏡の世界を映すカメラが、通常のカメラと同じ位置にあるからです。 よって鏡の枠内だけ残して通常世界を描画すれば、鏡を表現できます。

ScreenSpaceMappingについて

RenderTextureを使用する場合はScreenSpaceMapping用のシェーダを用意する必要があります。
tex2Dprojを使う必要があったりと少し特殊なシェーダになります。
解説されてる記事はたくさんありますので、詳しく知りたい方はググってみて下さい。
後述するgithubのサンプルプロジェクトにもScreenSpaceMappingのシェーダーもありますので、参考にしてください。

最適化

不要なときはレンダリングをスキップ

鏡も水面反射などと同様に、MainCameraに鏡のオブジェクトが映らないときは鏡用カメラのレンダリング処理自体が無駄になるので、
Renderer + OnWillRenderObjectなどと組み合わせて、映らないときはスキップするようにしてあげてください。
※OnWillRenderObjectのマニュアルページに書いてあるWater.csが参考になります。

注意点

実はこのRenderer + OnWillRenderObjectを使用する方法ですが、URPとHDRPではできません。。。
URPはCamera.Renderが、HDRPではOnWillRenderObjectがサポートされていません。
よって、このUWPもしくはHDRPで実施する場合はそれぞれの代替手段を考える必要があります。
ご留意ください。

追記

どうやらURPではOutput Textureがある場合はCamera.Renderは機能するっぽいです。

tsgcpp.hateblo.jp

鏡の枠内のみ描画する

実装の敷居は高くなりますが、Unityの安原様が書かれた記事が参考になると思います。
枠外の描画をどうしても避けたい方向けです。

[Unity]斜め(Oblique)の投影行列で空間を切り取る

おまけ

「発想」のところで記載した動画の様にアニメーションしながら世界を反転させる方法

f:id:tsgcpp:20200531212629g:plain
鏡の世界に切り替えるイメージ

鏡用カメラに使用した変換行列を以下のコードの様に、 Z方向のスケールを変換する行列をフレームごとに遷移させれば実現できます。

float value = curve.Evaluate(Time.time * bias) * 2 - 1;
Matrix4x4 zReverseMatrix = Matrix4x4.Scale(new Vector3(1, 1, value));  // Zのスケールをフレーム毎に遷移
GL.invertCulling = value < 0;  // ZScaleが反対になったらFrontface Cullingに切り替え

mirrorCamera.worldToCameraMatrix =
    mainCamera.worldToCameraMatrix *  // 通常のカメラ座標への位置 (4. の変換)
    mirror.localToWorldMatrix *       // 3. の変換
    zReverseMatrix *  // 2. の変換
    mirror.worldToLocalMatrix;        // 1. の変換

ちなみにMainCameraから見るとこんな感じになります

f:id:tsgcpp:20200531214453g:plain
MainCameraから見た世界が反転するところ

サンプルシーン

雑感

本当はVRでも実現したかったんですが、Unity2019.3.14f1もしくは2020.1.0b10でCamera.SetStereoViewMatrixを使うとおかしくなるため一旦保留にしました。
一応UnityにBug Reportとして出しましたが、対応はかなり時間がかかる気がします(実は1年前にも似たようなバグ報告をしたんですけど、対応されませんでした)。
やるとしたらカメラ2つを使うやり方になるかと思います。そのうち記事にしようと思います。

あと、UWPやHDRPでサポートされなくなるメソッドたちがあったりして、 使用するパイプラインごとにいろいろ変更しないといけないのが面倒かなって思います。

今回の記事の考え方を通して、皆さんの新しい発想の手助けになればと思います。

追記

[XR SDK][MOCKHMD] ERRORS APPEAR WHEN COPYING STEREO VIEW MATRICES FROM THE MAIN CAMERA TO A SUB CAMERA
IssueTrackerが登録されました。
よかったらVoteをお願いします!