MRが楽しい

MRやVRについて学習したことを書き残す

Unityで複数のゲームオブジェクトのスケールをメッシュ形状に合わせて調整する

本日は Unity の小ネタ枠です。
Unityで複数のゲームオブジェクトのスケールをメッシュ形状に合わせて調整する方法を記事にします。
f:id:bluebirdofoz:20210113180232j:plain

前提条件

前回記事の応用になります。
bluebirdofoz.hatenablog.com

サンプルシーン

以下のような様々な大きさや形状の3Dモデルを子オブジェクトに持つオブジェクトを作成しました。
f:id:bluebirdofoz:20210113180244j:plain

コード例

複数のゲームオブジェクトのメッシュ形状から 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;
    }
}

動作確認

スクリプトを子オブジェクトに持つオブジェクトに追加します。
f:id:bluebirdofoz:20210113180258j:plain

シーンを再生すると、子オブジェクトが 1m 立方のサイズに収まるようにオブジェクトが縮小されます。
更に子オブジェクトのメッシュ形状を内包する BoxCollider がオブジェクトに追加されます。
f:id:bluebirdofoz:20210113180312j:plain
f:id:bluebirdofoz:20210113180323j:plain
f:id:bluebirdofoz:20210113180333j:plain

2021/07/22追記

MeshFilter ではなく MeshRenderer を利用すると、ワールド座標の Bounds を取得することができます。
MeshRenderer を用いた改良版のスクリプトは以下の記事を参照ください。
bluebirdofoz.hatenablog.com