本日は Unity の小ネタ枠です。
Unityで複数のゲームオブジェクトのスケールをメッシュ形状に合わせて調整する方法を記事にします。
前提条件
前回記事の応用になります。
bluebirdofoz.hatenablog.com
サンプルシーン
以下のような様々な大きさや形状の3Dモデルを子オブジェクトに持つオブジェクトを作成しました。
コード例
複数のゲームオブジェクトのメッシュ形状から Bounds を計算して指定のスケールサイズに収めるサンプルコードを作成しました。
以下の処理を行います。
1.アタッチしたゲームオブジェクトの子オブジェクトを全てチェックする。
2.MeshFilter が設定されているオブジェクトからメッシュ形状の Bounds を取得する
3.取得したメッシュ形状の Bounds 全てを含むワールド座標系の Bounds を取得する
4.ワールド座標の Bounds の最大長の辺が指定のスケールに収まるようにローカルスケールを調整する
5.アタッチしたゲームオブジェクトのローカル座標系の Bounds を設定する
6.シーンで結果が確認できるように Bounds サイズの BoxCollider を設定する
・ControlChildObjBounds.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ControlChildObjBounds : MonoBehaviour { /// <summary> /// 子オブジェクトの統合Bounds /// </summary> public Bounds childObjBounds; void Start() { // オブジェクトのローカルスケールをオブジェクトが 1m 長に収まるよう調整する ChangeWorldBoundsSize(1.0f); // Boundsの大きさと形状が見た目に分かるようコライダーを追加する BoxCollider collider = this.gameObject.AddComponent<BoxCollider>(); // 計算されたバウンドボックスに合わせてコライダーの大きさと位置を変更する collider.center = childObjBounds.center; collider.size = childObjBounds.size; } /// <summary> /// ワールド座標での全体のバウンドサイズを元にローカルスケールを調整する /// </summary> public void ChangeWorldBoundsSize(float size) { // ワールド座標のバウンドサイズを計算する Bounds objBounds = CalcChildObjWorldBounds(this.gameObject, new Bounds()); // バウンドの最大長の辺の長さを取得する float maxlength = Mathf.Max(objBounds.size.x, objBounds.size.y, objBounds.size.z); // スケール調整の係数を取得する float coefficient = size / maxlength; // ローカルスケールを変更する this.transform.localScale = this.transform.localScale * coefficient; // ローカル座標でのバウンドサイズを計算する childObjBounds = CalcLocalObjBounds(this.gameObject); } /// <summary> /// 現在オブジェクトのローカル座標でのバウンド計算 /// </summary> private Bounds CalcLocalObjBounds(GameObject obj) { // 指定オブジェクトのワールドバウンドを計算する Bounds totalBounds = CalcChildObjWorldBounds(obj, new Bounds()); // ローカルオブジェクトの相対座標に合わせてバウンドを再計算する // オブジェクトのワールド座標とサイズを取得する Vector3 ObjWorldPosition = this.transform.position; Vector3 ObjWorldScale = this.transform.lossyScale; // バウンドのローカル座標とサイズを取得する Vector3 totalBoundsLocalCenter = new Vector3( (totalBounds.center.x - ObjWorldPosition.x) / ObjWorldScale.x, (totalBounds.center.y - ObjWorldPosition.y) / ObjWorldScale.y, (totalBounds.center.z - ObjWorldPosition.z) / ObjWorldScale.z); Vector3 meshBoundsLocalSize = new Vector3( totalBounds.size.x / ObjWorldScale.x, totalBounds.size.y / ObjWorldScale.y, totalBounds.size.z / ObjWorldScale.z); Bounds localBounds = new Bounds(totalBoundsLocalCenter, meshBoundsLocalSize); return localBounds; } /// <summary> /// 子オブジェクトのワールド座標でのバウンド計算(再帰処理) /// </summary> private Bounds CalcChildObjWorldBounds(GameObject obj, Bounds bounds) { // 指定オブジェクトの全ての子オブジェクトをチェックする foreach (Transform child in obj.transform) { if(!child.gameObject.activeSelf) { // 無効なゲームオブジェクトは無視する continue; } // メッシュフィルターの存在確認 MeshFilter filter = child.gameObject.GetComponent<MeshFilter>(); if (filter != null) { // オブジェクトのワールド座標とサイズを取得する Vector3 ObjWorldPosition = child.position; Vector3 ObjWorldScale = child.lossyScale; // フィルターのメッシュ情報からバウンドボックスを取得する Bounds meshBounds = filter.mesh.bounds; // バウンドのワールド座標とサイズを取得する Vector3 meshBoundsWorldCenter = meshBounds.center + ObjWorldPosition; Vector3 meshBoundsWorldSize = Vector3.Scale(meshBounds.size, ObjWorldScale); // バウンドの最小座標と最大座標を取得する Vector3 meshBoundsWorldMin = meshBoundsWorldCenter - (meshBoundsWorldSize / 2); Vector3 meshBoundsWorldMax = meshBoundsWorldCenter + (meshBoundsWorldSize / 2); // 取得した最小座標と最大座標を含むように拡大/縮小を行う if (bounds.size == Vector3.zero) { // 元バウンドのサイズがゼロの場合はバウンドを作り直す bounds = new Bounds(meshBoundsWorldCenter, Vector3.zero); } bounds.Encapsulate(meshBoundsWorldMin); bounds.Encapsulate(meshBoundsWorldMax); } // 再帰処理 bounds = CalcChildObjWorldBounds(child.gameObject, bounds); } return bounds; } }
動作確認
スクリプトを子オブジェクトに持つオブジェクトに追加します。
シーンを再生すると、子オブジェクトが 1m 立方のサイズに収まるようにオブジェクトが縮小されます。
更に子オブジェクトのメッシュ形状を内包する BoxCollider がオブジェクトに追加されます。
2021/07/22追記
MeshFilter ではなく MeshRenderer を利用すると、ワールド座標の Bounds を取得することができます。
MeshRenderer を用いた改良版のスクリプトは以下の記事を参照ください。
bluebirdofoz.hatenablog.com