TiltBrushにはミラー機能があって、左側にストロークを描くと右側にも鏡写しのようにストロークが描かれます。
これと同じような物(ただし、ストロークが2つ引かれるんじゃなくて表示上片方のストロークが両側にミラー表示される感じ)をUnityのシェーダで実現しようと思いましたが、若干悩んだので備忘録を残しておきます。
Unityのヒエラルキー上では”Sketch”という親のオブジェクトに”Stroke”という子オブジェクトが一杯付いてる状態を想定してます。
シェーダでミラー描画を実現したければ、1パス目で普通に描画して、2パス目でX座標をマイナスにして(カリングモードもFrontにする)描画すればいいだけなので、一見簡単そうです。
しかし実際にそうしたら、Strokeオブジェクトのローカル座標においてミラー描画されてしまいます。求めたいのはStroke(子)のローカル座標でもワールド座標でもなく、”Sketch”(親)オブジェクトのローカル空間におけるミラー座標です。
ですので、子Strokeローカル座標を親Sketchローカル座標に変換するマトリックスをシェーダに渡す事にしました。
C#コード(部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public void OnWillRenderObject() { if (renderer == null) { renderer = GetComponent<Renderer>(); } var parentT = transform.parent; var localToWorldMatrix = renderer.localToWorldMatrix; var worldToParentMatrix = parentT.worldToLocalMatrix; var localToParentMatrix = worldToParentMatrix * localToWorldMatrix; var parentToWorldMatrix = parentT.localToWorldMatrix; var worldToLocalMatrix = renderer.worldToLocalMatrix; var parentToLocalMatrix = worldToLocalMatrix * parentToWorldMatrix; Material strokeMaterial = SketchManager.Instance.strokeMaterial; strokeMaterial.SetMatrix("localToParentMatrix", localToParentMatrix); strokeMaterial.SetMatrix("parentToLocalMatrix", parentToLocalMatrix); } |
頂点シェーダコード(部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
uniform float4x4 localToParentMatrix; uniform float4x4 parentToLocalMatrix; void vert(inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.vertColor = v.color; o.uv_MainTex = v.texcoord.xy; //ローカル座標を親座標に変換 v.vertex = mul(localToParentMatrix, v.vertex); //X方向でミラー v.vertex.x = -v.vertex.x; //親座標をローカル座標に戻す v.vertex = mul(parentToLocalMatrix, v.vertex); } |
まず第一の罠ですが、TransformコンポーネントとRendererコンポーネントがそれぞれ localToWorldMatrixを持ってます。どっちを使えばいいのか?
リファレンスによれば、シェーダに渡すパラメータに使う場合はRendererコンポーネントのマトリックスを使う必要があります。何故なら、スタティックバッチングが適用されてる可能性があり、その場合はtransformのマトリックスでは正常に座標変換できないからです。
OnWillRenderObject()によって、オブジェクト毎にそれぞれ個別のシェーダパラメータを渡すことができます。
これを実行してみたら、一見正常に動いたようで、ストロークが複数になるとミラー表示がおかしくなりました。
なぜ?
調べてみたところ、Renderer.localToWorldMatrixを使っても、ダイナミックバッチングが適用されてるケースでは複数オブジェクトがまとめて描画されるため、個別にパラメータを渡せないので正常に座標変換できないらしいです。これが第二の罠です。
じゃあどうすればいいかと言うと、シェーダ内で自動で定義されている、ビルトインのシェーダ変数を使えばいいそうです。この中のUNITY_MATRIXとかは、ダイナミックバッチングとかも諸々考慮されていい感じに座標変換してくれるそうです。
というわけで、最終的にコードはこうなりました。
C#コード(部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void OnWillRenderObject() { if (renderer == null) { renderer = GetComponent<Renderer>(); } var parentT = transform.parent; var worldToParentMatrix = parentT.worldToLocalMatrix; var parentToWorldMatrix = parentT.localToWorldMatrix; Material strokeMaterial = SketchManager.Instance.strokeMaterial; strokeMaterial.SetMatrix("worldToParentMatrix", worldToParentMatrix); strokeMaterial.SetMatrix("parentToWorldMatrix", parentToWorldMatrix); } |
頂点シェーダコード(部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
uniform float4x4 worldToParentMatrix; uniform float4x4 parentToWorldMatrix; void vert(inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.vertColor = v.color; o.uv_MainTex = v.texcoord.xy; v.vertex = mul(unity_ObjectToWorld, v.vertex); v.vertex = mul(worldToParentMatrix, v.vertex); v.vertex.x = -v.vertex.x; //X方向でミラー v.vertex = mul(parentToWorldMatrix, v.vertex); v.vertex = mul(unity_WorldToObject, v.vertex); } |
Renderer.localToWorldMatrixを使うのをやめて、ビルトインシェーダ変数のunity_ObjectToWorldを使うようにしました。
これで正常にミラー描画が実現できました。