
Unityエディタ上で動作する簡単なスクリプトを書いてみる(LODGroup)
※前回の続きです。
前回Prefabから円形にオブジェクトを配置するサンプルを作りましたが、
今回は、それをベースに進めていきたいと思います。
今回は、負荷軽減でLODを使うことはありますが、それの一括登録スクリプトを作ってみようと思います。
そもそもLODを使うとき、標準だとLODGroupを使うと思いますが、
この登録が、オブジェクトが増えると非常に大変です。。。
そのあたりを簡単にしようというのが目的です。
LODGroupの追加
まず普通にLODGroupを追加して設定していきます。
AddComponentでLODで検索するとLODGroupが出てくるので、それを追加します。

LOD
LODGropが追加されたら、今回は、距離による表示/非表示だけにしたいので、「LOD 1」、「LOD 2」は削除します。

削除したら、Object Sizeの下にある「▶LOD 0」を押して「LOD 0」の設定を開きます。
開いたら、
「OjectSize」を「90」
「Transition(% Screen Size)」を「95」
にします。

で、普通にやる場合、、、「Renderers」の下にある「+」を押して、LOD対象とするオブジェクトを追加していく形になります。(っというか私が知らないだけかもしれない。。あったら教えて)

これを1つずつやっていたら、メンド(ゲフンゲフン 効率が悪いため、
一括で登録できるスクリプトを書きたいと思います。
LODGroupに一括登録できるスクリプトを作成
スクリプト機能としては、下記を実装します。
・Scriptを張り付けるオブジェクトにLODGroupが追加されている前提とする。
・Scriptが張り付いた配下にいるオブジェクトをLODGroupに一括登録できるようにする
・おまけで、指定の文字列が含まれるもののみLODGroupに一括登録できるようにする
では、進めていきます。
1.スクリプト作成
前回同様、
「Sample」フォルダ下の「Scripts」を選択し、右の部分で右クリックし「Create-C#Script」を選び、C#Scriptのベースを作成します。
作成したスクリプトの名前は、「RegistLODGroupScript」とします。

また、「RegistLODGroupScript」をダブルクリックし、VisualStudioを立ち上げてコードを書いていきます。
実際のコードは以下の通りです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RegistLODGroupScript : MonoBehaviour
{
[Header("基本設定")]
[SerializeField] int LODLevel;
[Header("登録するデータをオブジェクト名でフィルタする(正規表現)")]
[SerializeField] string NameFilterString;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
#if UNITY_EDITOR
[ContextMenu("UpdateLODGroup")]
public void UpdateLODGroup()
{
List<Renderer> render_objects = new List<Renderer>();
LODGroup lod_target = this.gameObject.GetComponent<LODGroup>();
if (this.gameObject != null && lod_target != null)
{
float object_size = lod_target.size;
GetChildrenRender(this.gameObject, NameFilterString, ref render_objects);
var src_lod = lod_target.GetLODs();
// 指定されたLODレベルより大きい値をLOD Group内にもっているかどうかを判断
if (src_lod.Length > LODLevel)
{
// 指定のLODレベルを初期化
src_lod[LODLevel].renderers.Initialize();
// 指定のLODレベルへ登録するオブジェクトの数分、配列を作成
src_lod[LODLevel].renderers = new Renderer[render_objects.Count];
// GetChildrenRenderで取得したレンダラーを登録。
for (int index = 0; index < render_objects.Count; index++)
{
src_lod[LODLevel].renderers[index] = render_objects[index];
}
}
// 更新したLODレベルを登録
lod_target.SetLODs(src_lod);
// バウンディング ボリュームを再計算
lod_target.RecalculateBounds();
// サイズを当初持っていた「LOD Group」のObject Sizeの値に戻す。
lod_target.size = object_size;
}
}
void GetChildrenRender(GameObject obj, string nameFilter, ref List<Renderer> renders)
{
// オブジェクトはいかにある子オブジェクト(子ノード)のTransformコンポーネントを取得
Transform children = obj.GetComponentInChildren<Transform>();
// 子オブジェクトにあるRendererを取得(なければ、rendererは、null)
Renderer renderer = children.GetComponent<Renderer>();
// 子オブジェクトにあるRendererを取得(なければnull)
if (renderer != null)
{
// 名前を確認
if (CheckTargetName(obj, nameFilter))
{
// LOD Group登録対象ならリストに追加
renders.Add(renderer);
}
}
// さらに子オブジェクトがある場合は再度検索する
if (children.childCount > 0)
{
foreach (Transform ob in children)
{
GetChildrenRender(ob.gameObject, nameFilter, ref renders);
}
}
}
bool CheckTargetName(GameObject obj, string nameFilter)
{
bool ret = true;
if (NameFilterString.Length > 0)
{
try
{
ret = System.Text.RegularExpressions.Regex.IsMatch(obj.name, NameFilterString);
}
catch
{
ret = false;
}
}
return ret;
}
#endif
}
コードを書いたら保存してUnityに戻ります。
(コードの説明は後ほど。。。)
このスクリプトを「SampleObject」に追加すると下記のようなUIが追加されます。

とりあえず、この状態のままで、「RegistLODGroupScript」の右上にある縦の「・・・」を押します。
そうするとメニューが現れ、一番下にある
「UpdateLODGroup」
を押すと、
「LODGroup」の「LOD 0」の中に、「SampleObjects」配下のオブジェクトが登録されていると思います。

この状態でプレビューしてオブジェクトに近づき、

過ぎ去る(円形外に出る)と。。。

LODが効いて消える。。。
今回の例ではあまり意味がないですが、
いくつかの区画を分けたようなワールドの時、
その区画内を一括で、LODで表示を消す時に使うとか想定で一度作ったやつだったりします。
※LODは、表示を消すだけなのでコライダーなどは残るので要注意。
2.UI説明
次に、UIの説明をしていきます。

[LOD Level]
これは、LOD 0、LOD 1といった具合にLODのLevelの番号を指定します。
今回は、「LOD 0」しかないので、「0」を指定します。
[Name Filter String]
これは、登録対象オブジェクトをフィルタで判別できるようにしています。(正規表現対応)
例えばここに、
「SampleCube_00.」と最後を半角の「 . 」(ドット)にすると、
登録されるオブジェクトが、
「SampleCube_001」~「「SampleCube_009」の9個登録されます。

事前に名前の命名規則など決めておくと、こういうフィルタなど設定しやすいのでお勧めです。
以上でUIの説明を終わります。
3.コード説明
では、コードを見ていきたいと思います。
前半は、前回とほぼ同様なので飛ばして、主要なところを見ていきます。
3-1.UpdateLODGroup
[ContextMenu("UpdateLODGroup")]
public void UpdateLODGroup()
{
List<Renderer> render_objects = new List<Renderer>();
LODGroup lod_target = this.gameObject.GetComponent<LODGroup>();
if (this.gameObject != null && lod_target != null)
{
float object_size = lod_target.size;
GetChildrenRender(this.gameObject, NameFilterString, ref render_objects);
var src_lod = lod_target.GetLODs();
// 指定されたLODレベルより大きい値をLOD Group内にもっているかどうかを判断
if (src_lod.Length > LODLevel)
{
// 指定のLODレベルを初期化
src_lod[LODLevel].renderers.Initialize();
// 指定のLODレベルへ登録するオブジェクトの数分、配列を作成
src_lod[LODLevel].renderers = new Renderer[render_objects.Count];
// GetChildrenRenderで取得したレンダラーを登録。
for (int index = 0; index < render_objects.Count; index++)
{
src_lod[LODLevel].renderers[index] = render_objects[index];
}
}
// 更新したLODレベルを登録
lod_target.SetLODs(src_lod);
// バウンディング ボリュームを再計算
lod_target.RecalculateBounds();
// サイズを当初持っていた「LOD Group」のObject Sizeの値に戻す。
lod_target.size = object_size;
}
}
ここで、LOD Groupの登録をしています。
LODGroup lod_target = this.gameObject.GetComponent<LODGroup>();
このスクリプトに登録されているLOD Groupのデータを取得しています。
※登録されているコンポーネントは、「GetComponent」で取得したいComponentクラスにキャストすると取得できます。
ない場合は、「null」が返ってくるので、それで判断します。
this.gameObject
は、thisが自分自身のスクリプトを表し、「this.gameObject」とすることで、自身が登録されているgameObjectを参照することができます。
float object_size = lod_target.size;
これは、「LOD Group」のObject Sizeを一時的に変数へ退避しています。
GetChildrenRender(this.gameObject, NameFilterString, ref render_objects);
これは、独自に作ったメソッドで、「this.gameObject」配下にある「Renderer」を持つオブジェクトを抽出する機能になります。
※LOD Groupの登録対象はObjectのComponentとして「Renderer」が登録されているObjectのみになるので対象のオブジェクトを抽出しています。
※この中は後述。
// 指定されたLODレベルより大きい値をLOD Group内にもっているかどうかを判断
if (src_lod.Length > LODLevel)
{
// 指定のLODレベルを初期化
src_lod[LODLevel].renderers.Initialize();
// 指定のLODレベルへ登録するオブジェクトの数分、配列を作成
src_lod[LODLevel].renderers = new Renderer[render_objects.Count];
// GetChildrenRenderで取得したレンダラーを登録。
for (int index = 0; index < render_objects.Count; index++)
{
src_lod[LODLevel].renderers[index] = render_objects[index];
}
}
// 更新したLODレベルを登録
lod_target.SetLODs(src_lod);
// バウンディング ボリュームを再計算
lod_target.RecalculateBounds();
// サイズを当初持っていた「LOD Group」のObject Sizeの値に戻す。
lod_target.size = object_size;
先ほどのメソッドから返ってきた「Renderer」が登録されているObjectを
上記処理で「LOD Group」へ登録しています。
この処理が実行する毎に、対象のLODレベル(今回はLOD 0」)を初期化して、1からすべて登録し直しています
3-2.GetChildrenRender
void GetChildrenRender(GameObject obj, string nameFilter, ref List<Renderer> renders)
{
// オブジェクトはいかにある子オブジェクト(子ノード)のTransformコンポーネントを取得
Transform children = obj.GetComponentInChildren<Transform>();
// 子オブジェクトにあるRendererを取得(なければ、rendererは、null)
Renderer renderer = children.GetComponent<Renderer>();
// 子オブジェクトにあるRendererを取得(なければnull)
if (renderer != null)
{
// 名前を確認
if (CheckTargetName(obj, nameFilter))
{
// LOD Group登録対象ならリストに追加
renders.Add(renderer);
}
}
// さらに子オブジェクトがある場合は再度検索する
if (children.childCount > 0)
{
foreach (Transform ob in children)
{
GetChildrenRender(ob.gameObject, nameFilter, ref renders);
}
}
}
GetChildrenRenderは、引数に渡したGameObject(obj)を走査して、「Renderer」が登録されているObjectを抽出して、その名前が登録対象なら引数のリスト(renders)に追加するメソッドです。
子要素がなくなるまで走査するようになっています。
3-3.CheckTargetName
bool CheckTargetName(GameObject obj, string nameFilter)
{
bool ret = true;
if (NameFilterString.Length > 0)
{
try
{
ret = System.Text.RegularExpressions.Regex.IsMatch(obj.name, NameFilterString);
}
catch
{
ret = false;
}
}
return ret;
}
UI上で、「Name Filter String」が指定されていた場合、
ret = System.Text.RegularExpressions.Regex.IsMatch(obj.name, NameFilterString);
上記で、引数に渡されたGameObject(obj)の名前が追加に該当する対象かをチェックして、対象でない場合はflaseを返し、それ以外はtrueを返すようにしています。
以上が、コードの説明です。
タネを明かせばオブジェクトをひたすら走査して登録に該当するオブジェクトを見つけて、それをLOD Groupに登録していくというだけの機能です。
さいごに
サンプルはLOD Groupでやりましたが、
意味さえ理解すれば、ほかのことも応用可能にです。
やることは、単純なものからやっていけば導入への障壁は、
そこまで高くないと思いますので、がんばってください!
