Making Tera

たまに書く不定期ブログ

Unityでインスタンシング

先日、参加中のサークルで「らせつ封魔伝」が完成しました!(製作期間7年っ)
自分はこのプロジェクトで3Dモデルやプログラムで協力させていただきました
www.dlsite.com

実はスケジュールを詰めたせいで村の草の設置が間に合っていませんでした
(次のアップデートで対応する予定です)


さて、この記事では草の配置に利用したインスタンスシングの実装についてさっとまとめましたので興味がある方はご覧ください

まずインスタンスシングって?って思う方のために
インスタンシング=「同じオブジェクトを大量に表示する機能」
です!


尚、導入前に以下についても検討の余地があると思われます

  • TerainのDetail機能による配置(今回の村はTerrainを使っていないっ)
  • Polybrushを利用する(我々が使用中のUnityバージョンでは古くて使えないっ)
  • 手動で配置(数が多いと重いし面倒)

(らせつでは化石のような開発環境で身動きが取れなくなっておりました…)

手順1 頂点群(配置データ)の作成

Polybrush等を利用してUnity上で頂点データを扱えればいいのですが、上記のようにUnityのバージョン縛りがあるので今回は3Dソフトで配置場所に点を打ち、その場所に草を生やすスクリプトを生成します

少しずつBlenderに移行したいと考えていたので練習を兼ねてBlenderのHair機能を利用してみました

手順は以下
・適用したい面を選択して頂点グループにしておく
・パーティクルも出ファイアを追加
・ヘアーに変更
・長さとセグメントを調整(最後に頂点にするので適当に)
・ヘア設定の頂点グループ>密度のところに先ほどの頂点グループをセットする

f:id:makingT:20201007204223p:plain

このモデファイアをメッシュに変換するといくつかの頂点+エッジになるのですが、変換直後に頂点モードに入ると根本の頂点のみが選択されている状態になるので選択を反転して削除してしまいます

f:id:makingT:20201007204558p:plain

頂点データを整頓したらデータ化に移ります
Exporterを作成するのがいいんでしょうが、一回しか使わないし…ということで秘儀テキストエディタの出番です
.objで書きだされたファイルは「v 100 10 20」のようにデータがずらーっと並んでいたりします
この部分を切り取る→いらない部分をテキストエディタで一斉置き換えという荒業

v 1 1 1
v 2 2 2
v 3 3 3

float[] hoge = new float[]{1,1,1,2,2,2,3,3,3};

に成形してそのままUnityのスクリプトに注入しました
(Ctrl+Hだけで数秒で終わると思います)

手順2 スクリプトの作成

まずは先ほどのデータをVectr3の配列に変換します
(利用している3Dソフトの座標系の違いにより、X座標にマイナスをかける必要がある点に注意してください)

スクリプト自体はUnityのマニュアルに載っているものがほぼそのまま流用できます
https://docs.unity3d.com/560/Documentation/ScriptReference/Graphics.DrawMeshInstancedIndirect.html

Vector4[] positions = new Vector4[instanceCount];
        for (int i=0; i < instanceCount; i++) {
            float angle = Random.Range(0.0f, Mathf.PI * 2.0f);
            float distance = Random.Range(20.0f, 100.0f);
            float height = Random.Range(-2.0f, 2.0f);
            float size = Random.Range(0.05f, 0.25f);
            positions[i] = new Vector4(Mathf.Sin(angle) * distance, height, Mathf.Cos(angle) * distance, size);
        }

の部分を自身の頂点データに置き換えるわけですが、サンプルではVector4なのでfloat一個分余っています
何かシェーダーに値を渡してもOKですしVector3に変更しても問題ありません

上記に加え らせつ封魔伝では「マップを縦横10等分して自身のまわりの9マスのみ表示」という変更を加えました

手順3 シェーダーの作成

シェーダーも先ほどのURLのものをほぼそのまま使えばいいので書くことがないのですが…。
テクニックと注意点を記載しておきます

大量のオブジェクトを配置したい場合「固定されたランダム値」が欲しくなることが多いです

スクリプトで求めた乱数をバッファを通して渡してもいいのですが、すでにXYZ座標があるので座標の小数部分を利用します
具体的には

 frac(data.x)

とすることで0~1の簡易ランダム値がお手軽に手に入ります

また、スクリプトから乱数を渡す際、ロードによって値が変わることを望まない場合は乱数生成前にシードを与えます

Random.InitState(123456);//シード
for (int i = 0; i < test.Length; i++)
    hoge = Random.Range(1f, 1.5f)));

次に注意点ですが、サンプルの

unity_WorldToObject = unity_ObjectToWorld;
unity_WorldToObject._14_24_34 *= -1;
unity_WorldToObject._11_22_33 = 1.0f / unity_WorldToObject._11_22_33;

の部分は簡易的な逆行列の求め方です
回転行列を適用した行列だと正常に求まらないので回転操作を行う場合はきっちりとしたInverse関数を作ってください
参考URL:
Incorrect normals on after rotating instances [Graphics.DrawMeshInstancedIndirect] - Unity Forum


サンプル内では4x4の行列をそのまま取り扱っているので、そこにランダム値を適用することで好きなように「拡大、縮小、回転、変形」が可能です
例えば大量に配置した草をY軸回転したい場合は回転行列を調査します

y axis rot
[c 0 -s 0]
[0 1 0 0]
[s 0 c 0]
[0 0 0 1]

対応した場所にランダム値θから求めたコサインとサインの値を入れるだけでバラバラにY軸回転するインスタンシングが完成します※

草を揺らす表現にはシェーダーの_Time関数とせん断行列を利用しています

まとめ

.objファイルの中身はテキストで扱いやすいのでちょっとした作業なら直接編集
公式マニュアルにインスタンシングの処理がほとんど記載してある
スクリプトからデータを好きに渡せるので 位置 色 回転 大きさ 揺れ方 等、大量に置くデータにも自由に個性を持たせられる

インスタンシング設置前
f:id:makingT:20201007225529p:plain
設置後
f:id:makingT:20201007225525p:plain


何かの参考になれば幸いです





※本気で最適化するならSinCosを静的処理で済ませて圧縮してバッファに入れることも検討するべきかもっ