TiltBrushにはミラー機能があって、左側にストロークを描くと右側にも鏡写しのようにストロークが描かれます。

これと同じような物(ただし、ストロークが2つ引かれるんじゃなくて表示上片方のストロークが両側にミラー表示される感じ)をUnityのシェーダで実現しようと思いましたが、若干悩んだので備忘録を残しておきます。

Unityのヒエラルキー上では”Sketch”という親のオブジェクトに”Stroke”という子オブジェクトが一杯付いてる状態を想定してます。

シェーダでミラー描画を実現したければ、1パス目で普通に描画して、2パス目でX座標をマイナスにして(カリングモードもFrontにする)描画すればいいだけなので、一見簡単そうです。

しかし実際にそうしたら、Strokeオブジェクトのローカル座標においてミラー描画されてしまいます。求めたいのはStroke(子)のローカル座標でもワールド座標でもなく、”Sketch”(親)オブジェクトのローカル空間におけるミラー座標です。

ですので、子Strokeローカル座標を親Sketchローカル座標に変換するマトリックスをシェーダに渡す事にしました。

C#コード(部分)

頂点シェーダコード(部分)

まず第一の罠ですが、TransformコンポーネントとRendererコンポーネントがそれぞれ localToWorldMatrixを持ってます。どっちを使えばいいのか?

リファレンスによれば、シェーダに渡すパラメータに使う場合はRendererコンポーネントのマトリックスを使う必要があります。何故なら、スタティックバッチングが適用されてる可能性があり、その場合はtransformのマトリックスでは正常に座標変換できないからです。

OnWillRenderObject()によって、オブジェクト毎にそれぞれ個別のシェーダパラメータを渡すことができます。

これを実行してみたら、一見正常に動いたようで、ストロークが複数になるとミラー表示がおかしくなりました。

なぜ?

調べてみたところ、Renderer.localToWorldMatrixを使っても、ダイナミックバッチングが適用されてるケースでは複数オブジェクトがまとめて描画されるため、個別にパラメータを渡せないので正常に座標変換できないらしいです。これが第二の罠です。

じゃあどうすればいいかと言うと、シェーダ内で自動で定義されている、ビルトインのシェーダ変数を使えばいいそうです。この中のUNITY_MATRIXとかは、ダイナミックバッチングとかも諸々考慮されていい感じに座標変換してくれるそうです。

というわけで、最終的にコードはこうなりました。

C#コード(部分)

頂点シェーダコード(部分)

Renderer.localToWorldMatrixを使うのをやめて、ビルトインシェーダ変数のunity_ObjectToWorldを使うようにしました。

これで正常にミラー描画が実現できました。

参考リンク

Unityでなるべくシェーディング処理を自作してみる