2年ぶりにQiitaに投稿。
3D空間上で、カメラアングルが変わっても常にこっちを向いていてほしい オブジェクトをつくりたいことがあります。
たとえば、2Dの板にテクスチャを貼り、それを常に正面を向かせることで、一枚絵を3D内で見せる,というテクニック(?)があります。こういう平面を板ポリゴンとかビルボードなどと呼んだりしますね。
この、ビルボードを常にカメラの方へ向ける行為ですが、シェーダで簡単に実装する方法を知ったので紹介してみようと思います。
やりかたはとっても簡単で、頂点シェーダで、クリッピング座標を計算する部分を以下の変えるだけです。
-OUT.vertex = UnityObjectToClipPos(IN.vertex); // 通常
+OUT.vertex = mul(UNITY_MATRIX_P, mul(UNITY_MATRIX_MV, float4(0, 0, 0, 1)) + float4(IN.vertex.x, IN.vertex.y, 0, 0)); // 常にカメラを向く
なぜこれで常に向きが一定になるのか、すこし解説してみようと思います。
まず、 変更前の、いつもやっている 座標変換の UnityObjectToClipPos
ですが、
最適化の分岐を無視するとだいたい以下のような実装になっています。
mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
うむ。モデル変換、ビュー変換、プロジェクション変換の行列をそれぞれ掛けているだけですね。
(入力の最後の要素に 1.0
を入れているのは、 3次元ベクトルを行列で座標変換する際に、移動(translate) も許すようにするおまじない)
上で登場する、座標変換のための行列の定数についてすこしだけふりかえってみます。
定数 | 意味 |
---|---|
UNITY_MATRIX_P | プロジェクション変換。カメラが映している領域を画面に投影する変換 |
UNITY_MATRIX_V | ビュー変換。ワールド座標から、カメラを原点に置いた場合の座標系への変換 |
unity_ObjectToWorld | いわゆるモデル変換。 ローカル座標からワールド座標への変換。UNITY_MATRIX_M という名前じゃないのが不思議(?) |
これを踏まえて、さきほどの、カメラを向きつつクリッピング座標へ変換するコードを もう一度見てみます。
mul(UNITY_MATRIX_P, mul(UNITY_MATRIX_MV, float4(0, 0, 0, 1)) + float4(IN.vertex.x, IN.vertex.y, 0, 0))
UNITY_MATRIX_MV
は、 VとMの行列をあらかじめCPU側で乗算したものです。 一度の描画における変化行列は不定なためです。 なので、実質、以下のコードと同等です。
mul(UNITY_MATRIX_P, mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(0, 0, 0, 1)) + float4(IN.vertex.x, IN.vertex.y, 0, 0)))
つまり、float(0,0,0.1)
に対して MとVの変換をすることで、 オブジェクトのどの頂点の計算もすべてビュー座標系(カメラを原点とした座標系)における原点ということに一旦してしまいます。(すべての頂点が原点にあれば、回転もくそもありません) その後、ビュー座標系における2D上でxとyを足すことで、ビュー座標系内で頂点の位置を合わせます。最後にP変換を被せて完成。
シェーダで実装するメリット
最後に、このように向きを変える処理をシェーダで実装するメリットについてです。
- トータルで計算コストが減る
- GPU側の処理はさして増えていない
- CPU側で毎フレームオブジェクトの向きを計算することも可能ですが、Unityの場合はメインスレッドに負荷が集りやすいため、全体としてリーズナブル。
- ゲームを再生しなくても、シーンビューなどすべての表示でこっちを向いてくれる。