【Unity】メッシュを結合するCombineMeshesを使って地形を結合してみる

【Unity】メッシュを結合するCombineMeshesを使って地形を結合してみる

以前別の記事でパーリンノイズを使って地形のオブジェクトを配置するサンプルを紹介しました。

簡単に滑らかな地形を作成できる点は良いのですが、ゲームオブジェクトを数千個、あるいは数万個インスタンス化する方法だったので、レンダリングで地獄を見ました。例えば以下の画像であれば急激に黄緑色が増えている部分で生成処理を行っています。

レンダリングコストがとてもお高い
レンダリングコストがとてもお高い

 

ゲーム実行中に統計情報を確認するとバッチ数が10万近くなっています。オブジェクトごとにマテリアルが割り当てられているので、同じマテリアルを使っていたとしてもこんなに負荷が重いものになります。これは実用するには辛いですね。

地獄めいたバッチ数
地獄めいたバッチ数

 

というわけでパフォーマンス面での調整としてメッシュを結合してみたいと思います。

Unityではメッシュを結合するための方法としてMeshクラスのCombineMeshesメソッドが提供されているので、こちらを使って結合してどれだけ処理が改善するか確認してみましょう。1年の初っ端から改善のお話なんて、とてもやる気に満ち溢れていますね(自画自賛)

 

 

環境

macOS 11.1 Big Sur

Unity2019.4.4f1

 

このページの前提となるもの

このページではメッシュの結合について解説しますが、その中で使うコードは以下の記事で作成したものを再利用します。

 

パーリンノイズを使って滑らかに変化する地形を作るサンプルで、ランダムな地形を作りたい時に便利です。ただし、パフォーマンス面で問題があったので、この記事でメッシュを結合する方法を使って使いやすい形にしたいと思います。

 

メッシュを結合するCombineMeshes

Unityでは複数のメッシュを結合する機能としてMeshクラスのCombineMeshesメソッドが提供されています。このメソッドを使用することで地形オブジェクトとして作成したたくさんのメッシュをひとつにまとめることができ、Batchesの数も大幅に減らすことができます。

まずはメッシュを結合する機能をスクリプトで作ってみましょう。Projectウィンドウの任意のフォルダでスクリプトを作成し、名前を『MeshCombiner』にします。

スクリプトの作成
スクリプトの作成

 

作成したスクリプトでは、ボタンを押した時にメッシュを結合する機能を作ります。サンプルコードは以下のようになっています。

フィールドとして、地形オブジェクトの親オブジェクトへの参照をアサインできるようにしています。以前の記事で作成した『FieldGenerator』で作成する地形オブジェクトの親オブジェクトと同じものをアサインすることを想定しています。

 

また、結合後のメッシュに適用するマテリアルもアサインするようにしています。

OnPressedCombineButton()のメソッドはボタンから呼び出すメソッドで、この中でメッシュを結合するメソッドであるCombineMesh()を呼んでいます。

CombineMesh()の流れはスクリプトリファレンスを参考にしています。

 

スクリプトリファレンスではクラスの宣言時に[RequireComponent]の属性を付けてMeshFilterとMeshRendererのコンポーネントを要求する形でしたが、ここでは動的にコンポーネントを付け外ししたかったのでメソッド内で存在を確認するようにしています。あればその参照を取得し、なければ新たにアタッチする、という流れはどちらのコンポーネントでも共通なので、CheckParentComponent()として切り出しています。

このメソッドでは型パラメータのTを使ってコンポーネントを引数のように渡せるようにしています。「where」として型の条件を指定していて、ここでは「Component」クラスまたはこのクラスを継承したクラスだけで使えるようにしています。

型パラメータについては以下の記事もご参照ください。

 

CombineMesh()に戻ると、GetComponentsInChildrenのメソッドを使って子オブジェクトのMeshFilterを取得しています。ただし、GetComponentsInChildrenを使うと親オブジェクト自身のMeshFilterも取得してしまうため、一度全て取得した後に配列の最初の値を取り除いています。

今回のケースだと親オブジェクトはMeshFilterのコンポーネントを持っていてもメッシュがアサインされていないので、中身がnullのメッシュを結合してしまうことになります。これだと『Combine mesh instance 0 is null.』という警告文が出てくるので、nullだとわかっているメッシュを除外しています。もしこの警告文を出したくないならメッシュがnullかどうかをチェックする処理を入れてもいいと思います。

結合する対象のMeshFilterを取得したら、結合するためのインスタンスであるCombineInstanceを作成します。この配列にメッシュの情報や位置の情報をセットしていき、処理が終わった子オブジェクトを非表示にしていきます。

結合が終わったら親オブジェクトのメッシュとして設定し、マテリアルを設定します。結合しているタイミングで親オブジェクトも非表示になっているため、処理が終わったらSetActive()を使って表示するようにしています。

マテリアルの設定を忘れると以下のようにピンク祭りが始まるのでお忘れなく(笑)

例のピンク
例のピンク

 

また、スクリプトでは削除ボタンが押された時の処理も実装しています。削除ボタンを押した時に地形オブジェクトを削除するようにしていますが、そのタイミングで親オブジェクトのコンポーネントもデタッチする(外す)ようにしています。

 

スクリプトのアタッチ

スクリプトを保存したらUnityエディタに戻ってアタッチします。ヒエラルキーウィンドウでコンテキストメニューを開き [Create Empty] から空のオブジェクトを作成します。名前は『MeshCombinerObj』にしました。

スクリプトのアタッチ
スクリプトのアタッチ

 

『MeshCombiner』のスクリプトをアタッチしたら、『Field Parent』に地形オブジェクトの親オブジェクトを、『Combined Mat』に結合後の地形にセットするマテリアルをそれぞれアサインします。

 

ボタンの作成

続いてメッシュ結合の処理を呼び出すボタンを作成します。ヒエラルキーウィンドウでコンテキストメニューを開き [UI] -> [Button] からボタンオブジェクトを作成します。地形作成の記事で既にボタンを作っている場合は複製すると簡単です。テキストは「結合」にしましたが分かりやすい名前ならなんでもOK。

作成したボタンは画面内の任意の場所に配置し、以下のようにメソッドを呼ぶ設定を行います。『OnPressedCombineButton』を呼び出すようにしましょう。

呼び出すメソッドの設定
呼び出すメソッドの設定

 

また、削除ボタンについても呼び出す処理を追加します。オブジェクトに『MeshCombinerObj』をアサインして、『OnPressedRemoveButton()』を呼び出すようにしています。

削除ボタンでもメソッドを追加
削除ボタンでもメソッドを追加

 

動作確認

ゲームを実行して、「生成」ボタン→「結合」ボタンの順に押してオブジェクトを結合してみましょう。例えば以下の例では50 * 50のサイズでオブジェクトを生成しています。生成直後はバッチ数が10355だったものが、結合後にはなんと11にまで減っています。GraphicsのFPSも10倍まで向上しています。実はTris(ポリゴン数)やVerts(頂点数)は増えてしまっているのですが、それでも処理にかかる時間は減っているのでゲームプレイが快適になります。

バッチ数が大幅に減った
バッチ数が大幅に減った

 

MeshColliderをアタッチする

今のままだとColliderがないのでオブジェクトが通り抜けていきます。

ボールがすり抜ける
ボールがすり抜ける

 

なので、結合したメッシュをそのままMeshColliderに渡してみましょう。CombineMesh()でメッシュを結合した後に、以下のようにMeshColliderをアタッチします。MeshColliderのsharedMeshのフィールドに結合したメッシュをセットしましょう。

 

この状態でゲームを実行してみると、結合したメッシュに沿ってColliderが働いているのがわかります。スクリプト内で作成したメッシュをそのままColliderとして使えるのは便利ですね。

地形として使えそう
地形として使えそう

 

実はまだ問題が……

さらっと流していましたが、実はまだまだ問題があったりします。

メッシュの結合はどうやら数が多いとうまくいかないようで、例えば200*200の大きさだと以下のように意図した通りに結合できていません。

結合したはずが不思議な形に……
結合したはずが不思議な形に……

 

見た感じだと地形のベースとなるオブジェクトが重なり合ってしまっているので、数が多い場合にはメッシュをいくつか分けて結合した方が良さそうですね。どこからうまくいかなくなるのかのラインを見極めるのは大変そうですが、結合するメッシュの数をスクリプトのフィールドから動的に変えられるようにして確認していくのがいいかもしれません。

また、このスクリプトでは何食わぬ顔でデフォルトのマテリアルをセットしましたが、実際に使う場合は地形に対応したマテリアルをセットしたいところです。草原のマス、砂漠のマス、といったように分けるためには頂点シェーダーなどを使ったマテリアルをセットするのが良いかもしれません。

というわけでこれらの課題については別のページを用意してじっくり対応したいと思います。

 

追記: 頂点シェーダーはなくてもいけました。マテリアルごとにメッシュをまとめることで生成時と同じように表示できます。

 

まとめ

CombineMeshesを使ってメッシュを結合するサンプルを紹介しました。メッシュをまとめることでCPUがGPUに対して命令する回数が減らせるので、結果的にフレームレートへの負荷を減らすことができます。

ただ、オブジェクト数が増えた時に意図した通りに結合できない問題や、マテリアルの問題があるので、そちらについては別のページで対応します。

なお、ここで紹介している内容は『Mesh Baker』というアセットを使うことで解決できちゃうので、急ぎならこちらのアセットを使うのがおすすめです。

 

無料版もあるのでこちらで試すのもいいかも。

 

 

パーリンノイズ関係やそれに関連するメッシュ関係の記事は以下のページでまとめています。

 

     

ゲーム開発の攻略チャートを作りました!

CTA-IMAGE

「ゲームを作ってみたいけど、何から手を付けていいか分からない!」


そんなお悩みをお持ちの方向けに、todoがアプリをリリースした経験を中心に、ゲーム作りの手順や考慮すべき点をまとめたe-bookを作成しました。ゲーム作りはそれ自体がゲームのように楽しいプロセスなので、「攻略チャート」と名付けています。


ゲームを作り始めた時にぶつかる壁である「何をしたら良いのか分からない」という悩みを吹き飛ばしましょう!