プロシージャルインタラクティブ草

Unity上で草を生やすシェーダを書きました.

f:id:FriendSea:20181030093918g:plain

github.com

コリジョンに応じて倒したり,草を刈ったりできます.

草を生やす

これはジオメトリシェーダを使っています.あらゆる面を草まみれにする.

f:id:FriendSea:20181030093244p:plain

Geometry Shader

ジオメトリシェーダでは描画するジオメトリの形を変更したり頂点数を増減させたりすることができます.
頂点シェーダで変換された頂点データが入力として与えられますが,このとき予め定めたプリミティブの単位でデータを受け取ることができます.例えばpoint,line, triangle等.
今回はtriangle, つまり三角形ポリゴンを入力として草となるポリゴンを生成してます.

草ポリゴン

今回は草1本を単純にポリゴン1枚で表現すこるとにします.元のメッシュの1ポリゴンにつき1本生成するとこんな感じ.

f:id:FriendSea:20181030094135p:plain

受け取ったtriangleの0番目の頂点の位置に草を生やしていますが,このメッシュでは位置が重なっているので同じ点に草が2本ずつ生えちゃう.まいっか.

草は常にカメラ方向を向くようにしてます.元のメッシュの法線方向に草を伸ばしているので,法線ベクトルと視線ベクトルの外積の方向に草の横幅をとるといい感じになる.

密度を上げる

このままだとスカスカなので元メッシュのポリゴン毎に草を何本か生やすことにします.入力の三角形を図の右側の様に分割して黒点のところに草を生やす.点がない部分は隣接するポリゴンが生やしてくれます.

f:id:FriendSea:20181030094154p:plain

図では1辺を4等分していますが,この分割数を変えて密度を調整しています.ただしジオメトリシェーダの出力用要素数の上限があるので,分割が多いと途中から描画できなくなります.
密度を上げる方法として,疑似乱数を使って入力ポリゴン内にランダムに生やす方法も試しました.しかし,あまりポリゴンの端の方には生えてくれずに偏って見えたので完全に整列させる方法をとっています.

不揃いにする

位置や高さが整列し過ぎなので疑似乱数を使って少し不揃いにします.急に草っぽくなりました.

f:id:FriendSea:20181030094214p:plain

乱数のシードとして草の位置に応じた元メッシュのUV座標を使っています.これなら草1本毎に違う値,毎フレーム同じ値が出てくれていい感じです.

風になびくようにする

見た感じ既にかなり草ですが,草は揺れてほしいです.これは時間の経過とともに頂点位置をズラせば簡単に表現できます.Unityでシェーダを書く場合は_Timeという定義済みの変数があり,実行開始からの時間を返してくます.便利.
草の先の頂点がsin関数に従って揺れるようにするとこんな感じ.

f:id:FriendSea:20181030094235g:plain

シェーディング

シェーダなのでシェーディングします.と言っても今回は環境光とシャドウしか扱っていません.
サーフェイスシェーダを変換したコードを追ってみると"ShadeSH9"という関数で環境光を取っているので今はそれを使うのがいいっぽいです.こういうのもっとわかりやすくドキュメントに書いといてほしい.
シャドウを受けるようにするにはForwardBaseのパスでシャドウマップをサンプリングする他に,ShadowCasterパスが必要になります.ShadowCasterが要るのはシャドウを落とす時だけだと勝手に思っていましたが,どうもスクリーンスペースのシャドウマップを作るので必要になるっぽいです.
環境光とシャドウだけ使ってシェーディングを行うと冒頭に載せた画像のような結果になります.

草を動かす

無事に草を生やせたので冒頭に載せたようにインタラクティブに動かします.

仕組み

草の傾きを決めるノーマルマップのようなテクスチャを用意し,これをシェーダでサンプリングして草を傾けています.
このテクスチャをキャラクターの動きに合わせていい感じに更新してやればリアクションのある草になるはずです.

草シェーダ

草のシェーダでに少し手を加えましょう.

草の先端の頂点を草マップ(なんて呼べば良いかわからないけどとりあえずこう呼ぶ)に合わせて移動させます. このときの変位ベクトルを草マップから取るわけですが,ノーマルマップを使う場合と同様に接空間での座標系に従います.
Unityではメッシュに対して適当に接線ベクトルを定めてシェーダに渡してくれるので.シェーダ側で法線と接線ベクトル,その外積の従法線ベクトルが得られます.この3つを接空間の基底として草マップから得られたベクトルを変換すればOKです.

f:id:FriendSea:20181030094435p:plain

図を描いていて気づきましたが,法線はデータ側で好き勝手に定義できちゃうので直行基底になるとは限りません.外積から出す従法線は一応正規化しておきます.

草マップ生成

あとはキャラクター等の位置に応じて草マップを生成すればうまく動くはずです. 草地を踏みしめたときの草の倒れ方として,とりあえずキャラクターを中心に放射状に倒れることにします.この倒れ方を作るノーマルマップを最初に作っちゃいました.

f:id:FriendSea:20181030094450p:plain:w300

これは球のノーマルマップを極座標系でrを反転させてやるとできます.   これをうまいこと合成すると草マップができるはずです.二通りの方法を試しました.

カメラで直接撮影する

f:id:FriendSea:20181030094511p:plain

乱暴ですがワールド座標とテクスチャ内の位置の関係などを考えないでいいので楽です.レイヤーマスクを使えば必要なものだけ映るようにできます.
草をはやしている平面に対してコリジョンがあったら,その位置に草を傾けるノーマルマップを生成してやればOKです. また,生成した草マップの傾きを少し減衰させたものを次のフレームでの合成時に用いることで,草の傾きが徐々に戻るような動きを作れます.

Graphics.Blitで合成

カメラを使った場合と同じことをGraphics.Blitで行っています.こっちのほうが素直だと思います.
これもコリジョンの位置に合わせて合成してます.
こっちの方法で動かしている草は今の所OpenGLでしか動かず,Unityエディタの描画APIをDX11などにすると変なことになります. 原因はそのうち調べます.

草刈り

頑張って生やした草ですが刈ってみます.草が刈られている領域をシェーダに渡し,シェーダで途中で切られたような形で描画すればいいです.ということでアルファチャンネルに草が刈られた長さを入れてます.
草マップを合成するときに使っているシェーダではアルファチャンネルの値を保持し続けるようにしているので,一度草を刈ればその状態がずっと続いてくれます. 草を刈りたいときはアルファチャンネルを上書きするシェーダを使います.カメラを使って草マップを作っている場合は斬撃エフェクト等にそのままアルファ上書きをくっつければ刈れます.

一番始めにの画像にある四角い箱が「草を刈るなにか」です.この例ではカメラを使ってマップを作っていて,TrailRendererで草を刈っています.

おしまい

草を生やして動かして刈りました.満足です.
今回動的な草を生やしたのはPlane1枚分だけで,面積を増やすと必要なRenderTextureも増えます.
超広い平原に無限に草が生えてるゲームとかどうやってんだろうね.