【Unity】鏡に映るは鏡の世界なり (Unityで鏡の実装)
概要
タイトル訳:Unityで鏡を作りたい! 今回紹介する考え方は応用が利くので、何かのお役に立てばと思います。
※Qiitaの記事と全く同じです
動作環境
- Unity 2019.4.0f1
原理
まず初めに、鏡に映るものをどうやってレンダリングするかを考えましょう。
発想
鏡の映像を撮りたいということで、 カメラに鏡の世界を撮ってきてもらいましょう!
※アニメーション中の影の動きは気にしないでください ※青暗い面は鏡の境界面を表現しています。
こちらのイメージ映像の通りに世界を反対にしましょう。 ここで重要なのは「鏡の面を境にして世界を反対にする」ことです。
実装
実現方法
鏡の境界を中心として、鏡面方向のスケールを-1にします。 ということで 射影変換行列 を使いましょう!
変換の順番は
- 鏡の位置を原点とする座標系に変換(鏡のローカル座標に変換)
- 原点を中心として、Z方向のスケールを-1に変換
1.
の逆変換3.
の後にできた世界をそのままレンダリング (通常のカメラ座標への変換)
上記変換を実施する行列をカメラのViewMatrixより先に実施されるように差し込んであげれば良いです。
以下はイメージです。表示している軸はワールド座標系の軸です。
この動画の通りカメラの位置や回転は変更する必要はありません。 世界だけ鏡の境界で反対にした後に映像を撮れば鏡の映像になります。
鏡用カメラのCulling Mask
新たにMirror
レイヤーを追加して、鏡用カメラのCulling Maskからは除外します。
鏡オブジェクトのレイヤーをMirror
にして、鏡用カメラにレンダリングされないようにしましょう。
変換を実施するコード
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;
ここまでの結果
早速、以下のシーンで鏡用カメラの映像を見てみましょう!
こうなります。。。
何が起こっているというと、モデルのポリゴンの向きが反対になってるんですね。
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; // 切り替え前に戻す
問題なく鏡の世界が描画されました。影の向きも鏡の世界のものになっていますね。
鏡の映像を鏡に反映
RenderTextureやDepthMaskを使って鏡に反映してください。 サンプルシーンを記事の最後に載せておきます。
違和感なさそうですね。
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
は機能するっぽいです。
鏡の枠内のみ描画する
実装の敷居は高くなりますが、Unityの安原様が書かれた記事が参考になると思います。
枠外の描画をどうしても避けたい方向けです。
[Unity]斜め(Oblique)の投影行列で空間を切り取る
おまけ
「発想」のところで記載した動画の様にアニメーションしながら世界を反転させる方法
鏡用カメラに使用した変換行列を以下のコードの様に、 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から見るとこんな感じになります
サンプルシーン
- https://github.com/tsgcpp/ZScaleInverseMirror-Unity/tree/NonXR
DepthMaskMirror.unity
はDepthMaskを使ったミラーRenderTextureMirror.unity
はRenderTextureを使ったミラー
雑感
本当は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をお願いします!