エフアンダーバー

個人開発の記録

Unityのエディタ拡張で頂点変換を可視化

久しぶりのブログ更新。
せっかくなので何か楽しそうなことをしたいと思い、 頂点が行列によって変換されていく様子をアニメーションさせてみました。 これで初学者も頂点変換が理解できる・・・かもしれない。

頂点変換アニメーション

こちらからソースコードをダウンロードして適当なEditorフォルダに突っ込めば準備完了。
あとはUnityエディタ上でゲームを実行し、 一時停止後にメニューのVertex Transformation > Enabledにチェックを入れればアニメーションが始まります。 シーンビューでカメラをグリグリしながら眺めてください。

f:id:fspace:20160711215319p:plain
レンダリング結果
f:id:fspace:20160711215235g:plain
頂点変換アニメーション

注意事項

  • 簡単なシーンで実行するようにしてください(パフォーマンスはあまり気にしていないので)
  • 基本的にMeshRendererにしか対応していません
    • SkinnedMeshRendererは一応動きますがMeshを抜き取っているだけなので期待通りの動きはしないと思います
  • カメラはメインカメラを使用します
    • 変更したい場合はソースコードを弄ってください
  • スクリーン座標は適当に縮小してあります
  • 本来、スクリーン座標は(Y軸が下向きのため)上下逆さまになりますがそのままにしてあります
    • 変更したい場合はソースコードを弄ってください
  • ライトの位置や向きなどは変更していないためゲーム画面と変換結果の見た目は異なります(あくまでも頂点座標の変換です)

Unity上の頂点変換

単純な頂点変換とは少し違う動きをするので、Unity上での頂点変換について少し補足。

Unityはシーンビューを見ればわかるように、ワールド空間では左手系(Z軸が奥向き*1)を採用しています。 しかし一方で、ビュー空間以降の変換はOpenGLに準拠しており、OpenGLはビュー空間において右手系(Z軸が手前向き)です。 そのためどこかで左手系を右手系へと変換する必要があります。

Unityではこれをビュー変換のタイミングで行っているようで、 原点かつ無回転のカメラを設置してビュー行列を取得すると、単位行列ではなく、Z軸を反転させる行列が返ってきます。 これがビュー変換時のアニメーションに含まれる「Z軸反転」の意味です。

また、OpenGLにおいてビュー空間までは右手系ですが、射影変換後の正規化デバイス空間は左手系です。 そのため射影変換時に右手系から左手系へと変換をします。 これが射影変換時のアニメーションに含まれる「Z軸反転」の意味です。

二回のZ軸反転によって結局Z軸がもとに戻るというよくわからない状態になっていますが、 ビュー変換までしかやらないといったこともエフェクトによってはあるので、 相殺させて変換をしないということはできないのです。

ちなみにZ軸反転のアニメーション時に視錐台(可視範囲)が手前に来るのは、 右手系に変換した座標を左手系の座標だと思って表示するとああ見えるよというだけであって、 実際には位置は動いていません(何言ってんのかよくわからないかもしれませんが・・・)。 現在の座標軸みたいなものを表示してそれを動かせばわかりやすかったのかもしれませんが、 コーディング中にはそこまで考えていませんでした・・・

完成までの道程

もともとさくっとコード書いて終わらせるつもりだったのですが、 思いのほか苦労したので躓いたところを書いておきます。

ワールド行列は変更不可

当初の設計としては各ワールド行列を、対象カメラのビュー行列や射影行列を乗算済みのものに置き換えるつもりでした。 Unityにおいてビュー行列や射影行列は変更できることを知っていたので、 ワールド行列も変更できるものだと思い込んでいたのですが、 スクリプトリファレンスを確認したところワールド行列は読み込み専用*2で計画破綻。 なんとかシェーダ変数だけでも置き換えられないかと試してみたのですがいい方法が見つからず断念。

仕方がないので今度はシーンビューのカメラのビュー行列を置き換えてみたのですが、どうもシーンビューの挙動がおかしくなりこれも断念。

どうしようかといろいろ探していたところで、Graphics.DrawMeshの引数にワールド行列を設定できるのを発見。 そこで、もともとのRendererを無効にして自前で描画する方針に切り替えます。

ポーズ中はUpdateが呼ばれない

Graphics.DrawMeshは影などをUnityに任せる都合上、描画段階よりも前に呼ぶ必要があります。 そのためリファレンスのサンプルではUpdate内で呼び出しているのですが、 今回は仕様上ポーズ中に描画命令を発する必要があり、ポーズ中はUpdateが呼ばれないので描画できません。

また今回のアニメーションはEditorApplication.updateにイベントハンドラを登録して動かしているのですが、 このタイミングでDrawMeshを呼び出すこともできません。 DrawMeshは描画命令の登録であり、その登録情報は描画終了後にクリアされます。 EditorApplication.updateと描画はタイミングが独立なので、 描画命令が過剰に発行されたり、逆に発行されなかったりしてしまいます。

そこで仕方なくカメラにスクリプトをくっつけ、OnPreCullを検知、 そのタイミングでDrawMeshを呼ぶことで対処しています。 OnPreCullくらいまでならまだDrawMeshが間に合うようです。

というか、そもそも状態更新の関数内で描画命令を発行する仕様自体がおかしいと思うのだけど、 どうにかならなかったんだろうか・・・

視錐台カリングの誤動作

今回の描画に使用するワールド行列は通常時に使われるものとは少し違います(例えば、行列の3行目にも値が入ります)。 そのためか、どうも視錐台カリングがうまく働きません。

とはいえ、Unityに視錐台カリングを制御するようなAPIはなさそうなので、 仕方なくMeshを複製してboundsに巨大な値を設定することでUnityをだましています。

バッチング時のバグ?

ワールド行列が特殊なためか動的バッチング時に正常に描画が行われませんでした (どうも行列の3行目の値が無視されて描画されているような印象)。

仕方なくすべてのMaterialを複製して描画することで動的バッチングの発生を回避しています。

SetDirtyの挙動

これについては仕様がいまいち理解できていないだけなのですが、 EditorUtility.SetDirtyを使ったときに再描画命令の発行のトリガーとなる対象とならない対象とあるようです。

とりあえずTransformに対して使うと再描画されるのでこれに対して呼び出しています。

背面カリングの無効化

実は最後のビューポート変換でY軸を反転させなかったのには理由があります。 それは反転させることによってMeshが裏返ってしまい、背面カリングによって描画されなくなってしまうためです。 Unityの背面カリングの設定はシェーダ内にあるためスクリプトから変更できません。

どうしようかとググってみたところ、両面メッシュをつくろうみたいな回答にたどり着いたので、 さすがにやってられるかと思い、反転を断念しました。

ソースコード

長いのでブログ内には埋め込みません。

下記リンクのGistよりご確認ください。

https://gist.github.com/f-space/074e0c116018aa8eccf5484c44e1de4b

おわりに

頂点変換について勉強していたころよく理解できなかった記憶があったのでつくってみたのですが、 これ見たところで理解できるかというと・・・う~ん。 まあアニメーション眺めているだけでも結構楽しいのでいいかな?

つくってみて改めて思ったのはUnityってハック向きじゃないですね。 ゲームエンジンでゲームと関係ないことしようとする自分が悪いのだけれど。 初期の段階でDrawMeshを使う方法と独自のマテリアルに置き換える方法で悩んだのですが、 今になって思うと後者の方が素直でよかったですね。

余談ですが、ソースコードがひとつのファイルに書かれているのは、 書き始めた時点では1ファイルに収まるくらいの分量になると思っていたからです。 結果的にはるかに超過してしまいましたが・・・

おまけ

実はもともとトップの画像にはユニティちゃんを採用するつもりだったのですがやめました。
理由は・・・見ればなんとなくわかると思います。

f:id:fspace:20160711215743p:plain
レンダリング結果
f:id:fspace:20160711215331g:plain
1カメ f:id:fspace:20160711215412g:plain
2カメ

ユニティちゃんライセンス

ユニティちゃんライセンス

このコンテンツは、『ユニティちゃんライセンス』で提供されています



執筆時のUnityのバージョン:5.3.5

*1:X軸を右向き、Y軸を上向きにとった場合

*2:物理演算等との間でずれが生じないようにするためかと思われる