なんかいろいろしてみます

Dec 5, 2017 - 3 minute read - HoloLens

HoloLensで利用できる3DモデルのBoundingbox作成方法

HoloLensでオブジェクトを操作するときに便利なBoundingboxの作成方法を書いています.

Boundingboxとは

Hololensのholographicアプリでオブジェクトの操作を行うときモデルの周りに枠が出てきます。 この枠をBoundingboxと呼びオブジェクトのサイズや移動などに利用しています。 自作のアプリでも操作の助けになるので利用できるようにしてみます。

実装のためには以下の2つの機能を合わせる必要があります。

  1. 対象のモデルのBoundingboxのサイズを取得する。
  2. 取得したBoundingboxを可視化する。

まず対象モデルの親オブジェクトにBoxColliderを作成し、対象モデルの大きさを適応します。 これにより対象モデルを囲う形でBoxColliderを設定する事ができます。

次に設定したboxcolliderと同じサイズ、位置にbox状の枠を表示することでBoundingboxの可視化が行えます。 すでにモデルにColliderが設定せれている場合にはBoundingboxの可視化だけで実現できます.

今回はモデルをColliderが設定されていないor動的にモデルを読み込んだ場合などを想定して,モデルのメッシュデータからBoundingboxを作成する方法を紹介します. これを利用することでモデルの操作範囲をHoloLensやImmersiveデバイスで直感的に確認できるようになります.

今回のBoundingbox作成方法はobjファイルなどのMeshFilterが存在しているモデルのみに適応できます. fbxなどのSkinnedMeshRendererを持っているモデルには適応できません.(たぶん後でfbx用にも作ります)

説明には自分が作成しているHoloLensModule内にある Boundingbox.csを元に説明します.

Boundingboxサイズ取得方法

meshfilter編

Boundingbox.csではスクリプトをアタッチされたGameObject以下のモデルに対してMeshFilterを探索しBoundingboxをBoxColliderとして設定します. そのため以下の処理を行っています.

  1. meshfilterを探索しBoundingboxを取得する
  2. 階層構造のモデルに合わせてBoundingboxを調整する

Boundingbox.cs内部では以下の関数を再帰的に呼び出すことでMeshFilterの探索とBoundingboxの算出を行っています.

 1 2 3 4 5 6 7 8 9101112131415161718192021222324252627282930313233343536373839404142
private bool MeshFilterBounds(GameObject obj,out Bounds outBounds)
{
    Bounds bound = new Bounds();
    bool initFlag = false;
    Bounds buf;

    for (int i = 0; i < obj.transform.childCount; i++)
    {
        GameObject objchild = obj.transform.GetChild(i).gameObject;
        MeshFilter filter = objchild.GetComponent<MeshFilter>();
        Matrix4x4 mat = Matrix4x4.TRS(objchild.transform.localPosition, objchild.transform.localRotation, objchild.transform.localScale);
        if (filter)
        {
            Vector3 center = mat.MultiplyPoint(filter.mesh.bounds.center);
            Vector3 size = mat.MultiplyVector(filter.mesh.bounds.size);
            size.Set(Mathf.Abs(size.x), Mathf.Abs(size.y), Mathf.Abs(size.z));
            if (initFlag == false)
            {
                bound.center = center;
                bound.size = size;
                initFlag = true;
            }
            else bound.Encapsulate(new Bounds(center, size));
        }
        if (MeshFilterBounds(objchild, out buf))
        {
            Vector3 center = mat.MultiplyPoint(buf.center);
            Vector3 size = mat.MultiplyVector(buf.size);
            size.Set(Mathf.Abs(size.x), Mathf.Abs(size.y), Mathf.Abs(size.z));
            if (initFlag == false)
            {
                bound.center = center;
                bound.size = size;
                initFlag = true;
            }
            else bound.Encapsulate(new Bounds(center, size));
        }
    }

    outBounds = bound;
    return initFlag;
}
  1. 自GameObject以下の子GameObjectからMeshFilterを探索します.
    • MeshFilter内にはMeshを覆う形でBoundsが定義されています.今回はこのデータをBoundingboxに利用します.
  2. MeshFilterが見つかった場合には内部のboundsを取得し現在のオブジェクトの位置,向きを補正します.
    • MeshFilterのデータは対象オブジェクトのみのデータ=ローカル座標データのためワールド座標への変換が必要になります.
  3. 再帰関数を利用してさらに子GameObjectのMeshFilterを探索します.
  4. 子GameObjectがなくなるまで探索を続け,最終的にワールド座標に変換したboundsデータを組み合わせてBoundingboxデータとします.

公開しているBoundingbox.csでは空のGameObjectにBoundingbox.csをアタッチし,その下にBoundingboxを表示させたいモデルを配置します. 可視化用のマテリアルをセットし再生するとBoxCollierとLineRendererが自動でアタッチされBoundingboxを表示することができます.

SkinnedMeshRenderer編

まだ実装できてないです。 fbx形式のモデルはアニメーションを行うためmeshfilterだけでは正確なBoundingboxを取得できません。 fbxはアニメーションに対応しているため,SkinnedMeshRendererを参照してアニメーション後のモデルの形状からBoundingboxを算出する必要があります.

Boundingboxの可視化方法

Boundingboxの可視化にはLinerenderを利用します.

 1 2 3 4 5 6 7 8 9101112131415161718192021222324252627282930313233343536
private LineRenderer linerenderer;
private BoxCollider boxcollider;
private Vector3[] linepoint = new Vector3[16];

public void isActive(bool flag, bool isBoxCal = false)
{
    if (isBoxCal)
    {
        Bounds bounds;
        MeshFilterBounds(gameObject, out bounds);
        boxcollider.center = bounds.center;
        boxcollider.size = bounds.size;
    }
    if (flag)
    {
        linepoint[0].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[1].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[2].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[3].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[4].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[5].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linepoint[6].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linepoint[7].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[8].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linepoint[9].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linepoint[10].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linepoint[11].Set(boxcollider.center.x + boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linepoint[12].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linepoint[13].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y - boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[14].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z + boxcollider.size.z / 2);
        linepoint[15].Set(boxcollider.center.x - boxcollider.size.x / 2, boxcollider.center.y + boxcollider.size.y / 2, boxcollider.center.z - boxcollider.size.z / 2);
        linerenderer.positionCount = linepoint.Length;
        linerenderer.SetPositions(linepoint);
    }
    else linerenderer.positionCount = 0;
}

モデルに変化があった場合にはBoundingboxを再計算し,結果をLineRendererで描画します.

結果は以下の画像のようになります.

まとめ

  • オブジェクトをまとめてBoundingboxを設定するといっきに移動できるので便利(特にHoloLens)
  • fbx形式はアニメーション前提なので処理負荷が大きそう
  • Microsoftが提供しているMRDesignLabsにもBoundingboxがあるので
  • HoloLensの機能を集めたHoloLensModule作ってます.