MRが楽しい

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

Meshのローカル座標とワールド座標のBoundsをそれぞれ取得する

本日は Unity の小ネタ枠です。
Meshのローカル座標とワールド座標のBoundsをそれぞれ取得する手順を記事にします。

MeshのBounds取得

Mesh の Bounds 情報をスクリプトから取得するには MeshFilter と MeshRenderer の2つのアクセス方法があります。
MeshFilter からはローカル座標の Bounds、MeshRenderer からはワールド座標の Bounds 情報が取得可能です。

MeshFilterからのローカル座標のBounds取得

ローカル座標の Bounds は以下のように取得します。

Mesh mesh = GetComponent<MeshFilter>().mesh;
Bounds bounds = mesh.bounds;

docs.unity3d.com
docs.unity3d.com
docs.unity3d.com

MeshRendererからのワールド座標のBounds取得方法

ワールド座標の Bounds は以下のように取得します。

Renderer renderer = GetComponent<Renderer>();
Bounds bounds = renderer.bounds;

docs.unity3d.com
docs.unity3d.com

サンプルスクリプト

以下の記事で作成した Mesh の bounds をもとにワールド座標のスケールを調整するスクリプトを改修してみます。
bluebirdofoz.hatenablog.com

スクリプトは以下の処理を行います。
1.アタッチしたゲームオブジェクトの子オブジェクトを全てチェックする。
2.MeshRenderer が設定されているオブジェクトからメッシュ形状の Bounds を取得する
3.取得したメッシュ形状の Bounds 全てを含むワールド座標系の Bounds を取得する
4.ワールド座標の Bounds の最大長の辺が指定のスケールに収まるようにローカルスケールを調整する
5.アタッチしたゲームオブジェクトのローカル座標系の Bounds を設定する
6.シーンで結果が確認できるように Bounds サイズの BoxCollider を設定する
・RendererControlChildObjBounds.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RendererControlChildObjBounds : 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;
            }

            // メッシュレンダラーの存在確認
            MeshRenderer renderer = child.gameObject.GetComponent<MeshRenderer>();

            if (renderer != null)
            {
                // フィルターのメッシュ情報からバウンドボックスを取得する
                Bounds meshBounds = renderer.bounds;

                // バウンドのワールド座標とサイズを取得する
                Vector3 meshBoundsWorldCenter = meshBounds.center;
                Vector3 meshBoundsWorldSize = meshBounds.size;

                // バウンドの最小座標と最大座標を取得する
                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:20210722231742j:plain

MeshFilter で Bounds 情報を取得していた個所を MeshRenderer で取得することでよりシンプルに記述できました。
指定のスケールサイズに収めたい子オブジェクトを持つオブジェクトに、スクリプトを設定します。
f:id:bluebirdofoz:20210722231756j:plain

実行確認

シーンを再生して動作を確認します。
f:id:bluebirdofoz:20210722232206j:plain

シーンの再生と同時に、子オブジェクトが 1m 立方のサイズに収まるようにオブジェクトが縮小されます。
f:id:bluebirdofoz:20210722232218j:plain