MRが楽しい

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

Blenderでの頂点結合をマウス操作で行う

本日は Blender の技術調査枠です。
書籍「Blender標準テクニック[ローポリキャラクター制作で学ぶ3DCG]」で紹介されていた小ワザを試してみます。

頂点同士の結合を行う際、これまで学んだ方法だと以下の手順で実施する必要があります。
1.結合する片方の頂点を選択する。
2.結合するもう片方の頂点をShiftを押しながら選択する。
3.二つの頂点が選択されている状態で Alt+Mキー -> 結合 を行う。
f:id:bluebirdofoz:20180117001133j:plain
一つ二つの結合であれば上記の方法でも問題ありませんが、十個、ニ十個となってくると非常に面倒です。

そこで以下の設定を行います。
1.スナップボタンをクリックで有効化し、「スナップする要素」で「頂点」を選択する。
f:id:bluebirdofoz:20180117001141j:plain
2.「メッシュ」から「重複頂点の自動結合」にチェックを入れる。
f:id:bluebirdofoz:20180117001156j:plain

この設定を行った状態で結合したい頂点を選択し、ドラッグ操作でもう一方の頂点に近づけます。
f:id:bluebirdofoz:20180117001205j:plain
すると、ある程度頂点に近づくと、キャプチャのように頂点が頂点に吸い付きます。

この状態で、左クリックでドラッグをやめると、頂点と頂点が結合されます。
f:id:bluebirdofoz:20180117001212j:plain
複数の頂点を結合する際はこの方法を用いると、直感的かつスピーディに作業が行えそうです。

mmd4mecanimで変換したモデルにhumanoidリグを適用する

本日は MMD の技術調査枠です。
以前、mmd4mecanimでUnityにMMDモデルを取り込む方法について調査しました。
bluebirdofoz.hatenablog.com

MMDforUnityの変換で実施したhumanoidリグの反映をこちらの変換モデルでも試してみます。
bluebirdofoz.hatenablog.com

テスト用プロジェクトとして以下の記事で作成したプロジェクトを利用します。
bluebirdofoz.hatenablog.com

折角なので、講座を見ながら Unity 2017.2 で作り直しました。
f:id:bluebirdofoz:20180116021011j:plain
ここからユニティちゃんのキャラクタをMMDモデルに差し替えます。

mmd4mecanimの記事を参考にMMDモデルをUnityに取り込みます。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

サンプルモデルに今流行りのバーチャルユーチューバー、キズナアイのモデルを利用しました。
・download-page - Kizuna AI Official Website
 http://kizunaai.com/download-page/

今回はUnityちゃんのモーションを利用するので、mmd4mecanimでのモーション取り込みは不要です。
f:id:bluebirdofoz:20180116021124j:plain

モデルを取り込みました。Unityちゃんとモーションを共有するため、humanoidリグを設定します。
fbx ファイルを選択して Inspector から Rig タグを開き、Animation Type を「humanoid」に変更します。
変更したら Apply をクリックして変更を反映します。
f:id:bluebirdofoz:20180116021132j:plain

変更が完了したら、各ボーンの設定が正しく行われているかを確認するため、Configure をクリックします。
f:id:bluebirdofoz:20180116021141j:plain

以下のような関連付けになっていれば問題なさそうです。
mmd4mecanimで取り込んだモデルであれば名前は一致するはずです。
f:id:bluebirdofoz:20180116021149j:plain
(数字部分はモデルによって異なります)

fbx モデルをシーンに取り込み、Unityちゃんと同じセットアップを行います。
・Transform の設定
・Controller の設定
・Rigitbody のアタッチ
・CapsuleCollider のアタッチ
・UnityChanControlScriptWithRgidBody のアタッチ
f:id:bluebirdofoz:20180116021829j:plain
Unityちゃんは無効化しておきます。

ゲームを起動して Enter キーを押下すると……。
f:id:bluebirdofoz:20180116021840j:plain
成功です。MMDキャラクタをUnityちゃんのモーションで動作させることができました。


ただし……。
以下の記事でも言及した通り、厳密にはMMDモデルとhumanoidモデルのボーン構造は腰部分の構造が異なります。
bluebirdofoz.hatenablog.com
このため、腰回りのモーションが正確に再現されないことがあります。

Blender標準テクニック[ローポリキャラクター制作で学ぶ3DCG]を試す その29(指定方向のミラー)

本日は書籍「Blender標準テクニック[ローポリキャラクター制作で学ぶ3DCG]」の実施枠です。

ドレス衣装モデリングの最後になります。
f:id:bluebirdofoz:20180115000408j:plain
今回は衣装に付属するリボンを作成していきます。

まずは胸中央のリボンです。
押出等の今まで通りの手順で作成していきます。
f:id:bluebirdofoz:20180115000435j:plain

スカートの側面にも同じようなリボンがあるので、今作成したリボンを複製(Shift+Dキー)して流用します。
しかし複製した状態だとミラー反映前の左半分のメッシュのみです。
側面のリボンは横向きなのでY軸方向にミラーする必要があります。
f:id:bluebirdofoz:20180115000445j:plain

リボンの結び目を中心にミラーさせるため、まずはオブジェクトの原点を設定します。
オブジェクトモードで原点とする位置に3Dカーソルを設定し、Shift+Ctrl+Alt+Cキー -> 原点を設定を選択します。
f:id:bluebirdofoz:20180115000455j:plain

原点を設定したら、ミラーモディファイアの軸設定を Y に切り替え、ミラーオブジェクトを空欄にします。
これで原点を中心にY軸方向にミラーが行われるようになります。
f:id:bluebirdofoz:20180115000506j:plain

後は位置を合わせて、調整を行います。オブジェクト原点の位置でミラーリングをしています。
位置合わせの際はオブジェクトモードでオブジェクトそのものを移動させることに注意です。
f:id:bluebirdofoz:20180115000514j:plain

同じようにソックスのリボンを作成したら、ドレス衣装のモデリングは完了です。
f:id:bluebirdofoz:20180115000525j:plain

最後にドレス衣装として作成したオブジェクトを全て結合(Ctrl+Jキー)します。
f:id:bluebirdofoz:20180115000539j:plain

以前説明した通り、オブジェクトのモディファアは最後に選択したもののみが残ります。
リボン等のミラーモディファイアは適用してから結合しています。
f:id:bluebirdofoz:20180115000550j:plain

ARマーカーをHoloLensで利用する

本日は HoloLens の技術調査枠です。
HoloLens で Vuforia を利用し、ARマーカー上にオブジェクトを表示します。
Vuforia Developer Portal |

事前にARマーカーとして利用する画像ファイルを用意しておく必要があります。
JPEG もしくは PNG でファイルサイズが 2MB 以下であるものが利用可能です。
f:id:bluebirdofoz:20180114233317j:plain

Vuforia 自体は以前 android アプリで試したことがあり、アカウントは取得済みです。
ログインを行い、ライセンスキーを取得するところから始めます。
f:id:bluebirdofoz:20180114233326j:plain

以下の通り、既にライセンスキーを取得済みであれば、これを開いてライセンスキーの文字列をコピーします。
f:id:bluebirdofoz:20180114233337j:plain

未作成であれば Get Development Key もしくは Buy Deployment Key でライセンスキーを取得します。
Development のみ無料です。Development の場合、個人利用する自作アプリでのみ利用可能です。
f:id:bluebirdofoz:20180114233345j:plain

次にライセンスキーを取得したら、マーカーの登録を行います。
まずはマーカーを登録するデータベースを作成します。TargetManaer を選択し、Add Database をクリックします。
f:id:bluebirdofoz:20180114233353j:plain

データベースの作成画面が開きます
データベースの名前を設定し、Type で Device を選択して Create をクリックします。
f:id:bluebirdofoz:20180114233406j:plain

データベースを作成したら開きます。
するとデータベースの設定画面が開くので、Add Target をクリックします。
f:id:bluebirdofoz:20180114233446j:plain

マーカーの登録画面が開きます
ARマーカーの形によって Type を選択します。今回はただの画像なので SingleImage を選択します。
File に用意した画像を選択します。
Width は利用するマーカーの大きさを設定します。単位はメートルです。
例えば 2 を設定すると Unity のシーン内でマーカーは 2m × 2m の物体として認識されます。
Name には選択した画像のファイル名が自動で入力されます。
最後に Add をクリックして登録します。
f:id:bluebirdofoz:20180114233453j:plain

マーカーが作成されるので、選択して Download Database を選択します。
f:id:bluebirdofoz:20180114233501j:plain
このとき、画像の認識のしやすさが星マークで評価されています。
星5つならば認識しやすい画像という事です。特徴点が多くコントラストが高い画像程よいようです。

プラットフォームの選択画面が表示されます。
Unity Editor を選択し、Download をクリックします。
f:id:bluebirdofoz:20180114233516j:plain


実際に HoloLens で試すため、Unity のテストプロジェクトを作成します。
f:id:bluebirdofoz:20180114233524j:plain

Unity 上で Vuforia を利用するため、以下の Download Unity Extension からSDKパッケージを取得します。
SDK Download | Vuforia Developer Portal
f:id:bluebirdofoz:20180115000719j:plain

Vuforia からダウンロードした SDK と ARマーカー の unitypackage をインポートします。
f:id:bluebirdofoz:20180114233602j:plain

Vuforia からダウンロードしたマーカーの unitypackage をインポートします。
f:id:bluebirdofoz:20180114233612j:plain

Vuforia/Prefabs ディレクトリにある ARCamera.prefab と ImageTarget.prefab をシーンに配置します。
f:id:bluebirdofoz:20180114233621j:plain

最初に ARCamera の設定を行います。
配置した ARCamera を選択し、Inspector から Open Vuforia Configuration を選択します。
f:id:bluebirdofoz:20180114233630j:plain

App License Key の欄に取得したライセンスキーをペーストします。
HoloLens のカメラを利用するので Digital Eyewear の欄を、以下の通り設定します。
EyewearType:Optical See-Through
SeeThroughConfig:HoloLens
また、Datasets の欄に、読み込んだデータベース名が表示されているので Load ~ Database と Active にチェックを入れます。
f:id:bluebirdofoz:20180114233650j:plain

ARCamera の Inspector に戻り、Central Anchor Point に MixedRealityCameraParent 内の MixedRealityCamera を設定します。
f:id:bluebirdofoz:20180114233638j:plain

次に ImageTarget の設定を行います。
配置した ImageTarget を選択し、Inspector の Database と ImageTarget を登録したマーカーに設定します。
f:id:bluebirdofoz:20180114233702j:plain

読み込み時のままだと画像の Texture Shape が Cube になっているため、2D に変更して Apply を実行します。
f:id:bluebirdofoz:20180114234229j:plain

以上でマーカー認識の準備は完了です。
マーカーが認識できたことを確かめるため、マーカーの上に Cube オブジェクトを配置します。
これで HoloLens 上で実行してマーカーを見ると Cube オブジェクトが表示される……ハズですが、何故かマーカーを見てみても Cube は表示されず……。
(追記)
成功しました。原因は Cube オブジェクトを ImageTarget オブジェクトの子オブジェクトにしていなかったという凡ミスでした。
f:id:bluebirdofoz:20180115000016j:plain

複数バージョンのUnityを同時にインストールする

本日は Unity の調査枠です。
過去プロジェクトをビルドするため、Unity 5.6 を改めて利用する機会がありました。
この際、そのまま Unity 5.6 をインストールするとインストール済みの Unity は新旧に関わらず上書きされてしまいます。

別バージョンの Unity を同じPCで利用するにはインストールフォルダを分ける必要があります。
以下のページを参考に実施しました。
docs.unity3d.com

過去バージョンのインストーラは以下から取得します。
unity3d.com

取得した Unity 5.6.5 のインストーラを起動します。
f:id:bluebirdofoz:20180113212733j:plain

インストールディレクトリの選択で、デフォルトの Unity ディレクトリを指定せず。
別バージョン用の新規ディレクトリを指定します。
f:id:bluebirdofoz:20180113212854j:plain

インストールディレクトリの選択までは以前紹介した手順と同様です。
bluebirdofoz.hatenablog.com

インストールを完了すれば、別バージョンの Unity が利用できます。
f:id:bluebirdofoz:20180113212903j:plain

SharingWithUNETについてまとめる その7

本日は HoloToolKit の調査枠です。
SharingWithUNET のサンプルシーンを確認していきます。
bluebirdofoz.hatenablog.com

今回は HologramCollection オブジェクトにある SharedCollection について機能を確認します。
f:id:bluebirdofoz:20180112005351j:plain

SharedCollection は本オブジェクト自体には何の機能もありません。
アタッチされているスクリプトは以下の通り空クラスです。

・SharedCollection.cs

    /// <summary>
    /// This script exists as a stub to allow other scripts to find 
    /// the shared world anchor transform.
    /// このスクリプトはスタブとして存在し、他のスクリプトが共有世界のアンカー変換を見つけることを可能にします。
    /// </summary>
    public class SharedCollection : SingleInstance<SharedCollection>
    {

    }

しかし、本オブジェクトは各ホストの共通座標を示すという重要な役割を担います。
本オブジェクトは SharingWithUNET のシーン内で以下のように、検出することができます。
・UNetAnchorManager.cs

        private bool CheckConfiguration()
        {
(中略)
            // SharedCollectionのインスタンスを取得する。
            if (SharedCollection.Instance == null)
            {
                // SharedCollectionのインスタンスを取得できない場合、エラー
                Debug.Log("No SharedCollection found in scene");
                return false;
            }

#if UNITY_WSA
            // SharedCollectionのインスタンスを持つゲームオブジェクトを取得する。
            objectToAnchor = SharedCollection.Instance.gameObject;
(中略)
#endif

            return true;
        }


サーバ側は以下の UNetAnchorManager の ExportAnchorAtPosition 関数で、HologramCollection オブジェクトにアンカーを設定します。
・UNetAnchorManager.cs

        /// <summary>
        /// Createx and exports the anchor at the specified world position
        /// Createxは、指定されたワールド位置にアンカーをエクスポートします。
        /// </summary>
        /// <param name="worldPos">The position to place the anchor(アンカーを配置する位置)</param>
        private void ExportAnchorAtPosition(Vector3 worldPos)
        {
            // Need to remove any anchor that is on the object before we can move the object.
            // オブジェクトを移動する前に、オブジェクト上にあるアンカーを削除する必要があります。
            // SharedCollectionのインスタンスを持つゲームオブジェクトに設定されたアンカーを取得する。
            WorldAnchor worldAnchor = objectToAnchor.GetComponent<WorldAnchor>();
            if (worldAnchor != null)
            {
                // 既存のワールドアンカーが存在する場合

                // 既存のワールドアンカーを破棄する。
                DestroyImmediate(worldAnchor);
            }

            // Move the object to the specifid place
            // SharedCollectionのインスタンスを持つゲームオブジェクトを指定された場所に移動する。
            objectToAnchor.transform.position = worldPos;

            // Attach a new anchor
            // 新しいアンカーをアタッチする。
            worldAnchor = objectToAnchor.AddComponent<WorldAnchor>();

            // Name the anchor
            // アンカーに名前を付ける。
            exportingAnchorName = Guid.NewGuid().ToString();
            Debug.Log("preparing " + exportingAnchorName);

            // Register for on tracking changed in case the anchor isn't already located
            // アンカーがまだ位置していない場合に変更されたトラッキングのために
            // イベントとしてWorldAnchor_OnTrackingChangedを登録する。
            worldAnchor.OnTrackingChanged += WorldAnchor_OnTrackingChanged;

            // And call our callback in line just in case it is already located.
            // すでにコールバックされている場合に備えて、コールバックをオンラインで呼び出すことができます。
            WorldAnchor_OnTrackingChanged(worldAnchor, worldAnchor.isLocated);
        }


ホスト側は以下の UNetAnchorManager の ImportComplete 関数で、HologramCollection オブジェクトに受信したアンカーを設定します。
・UNetAnchorManager.cs

        private void ImportComplete(SerializationCompletionReason status, WorldAnchorTransferBatch wat)
        {
            // インポートが成功したかどうかをチェックする。
            if (status == SerializationCompletionReason.Succeeded && wat.GetAllIds().Length > 0)
            {
                // インポートが成功し、かつアンカーが1つ以上の場合
                Debug.Log("Import complete");

                // 1つ目のアンカー名を取得する。
                string first = wat.GetAllIds()[0];
                Debug.Log("Anchor name: " + first);

                // SharedCollectionのインスタンスを持つゲームオブジェクトに設定されたWorldAnchorを取得する。
                WorldAnchor existingAnchor = objectToAnchor.GetComponent<WorldAnchor>();
                if (existingAnchor != null)
                {
                    // WorldAnchorが存在した場合

                    // 既存のWorldAnchorを削除する
                    DestroyImmediate(existingAnchor);
                }

                // SharedCollectionのインスタンスを持つゲームオブジェクトにインポートしたアンカーを設定する。
                WorldAnchor anchor = wat.LockObject(first, objectToAnchor);
                // オブジェクトのトラッキング状態が変化したときのイベントにAnchor_OnTrackingChangedを設定する。
                anchor.OnTrackingChanged += Anchor_OnTrackingChanged;
                // ワールドアンカーが存在する場合は即座にWorldAnchorManagerに記録する。
                Anchor_OnTrackingChanged(anchor, anchor.isLocated);

                // インポート実行中フラグを落とす。
                ImportInProgress = false;
            }
            else
            {
                // if we failed, we can simply try again.
                // 失敗した場合は、もう一度やり直すことができます。
                // 更新実行フラグを立てる。
                gotOne = true;
                Debug.Log("Import fail");
            }
        }


Player.prefabなど他のオブジェクトはこの HologramCollection オブジェクトを検出することで共通の座標にオブジェクトを配置できます。
・PlayerController.cs

        /// <summary>
        /// 起動時処理
        /// </summary>
        private void Start()
        {
            // シェアリング空間のインスタンスがnullのとき、このプレイヤーインスタンスを破棄する。
            if (SharedCollection.Instance == null)
            {
                Debug.LogError("This script required a SharedCollection script attached to a gameobject in the scene");
                Destroy(this);
                return;
            }
(中略)
            // シェアリング空間のTransform値を設定する。
            sharedWorldAnchorTransform = SharedCollection.Instance.gameObject.transform;
            // 自身のオブジェクトの親Transformをシェアリング空間のTransform値と同じにする。
            transform.SetParent(sharedWorldAnchorTransform);
        }


また本オブジェクトは子オブジェクトとして、アンカーデバッグ用のUIオブジェクトを持ちます。
f:id:bluebirdofoz:20180112005330j:plain
SharingWithUNET のシーン内で本UIの表示位置が現在のアンカー位置になります。
シェアリングが上手くいかないときは、これで全てのホストでアンカー位置が同じか確認するとよいです。

SharingWithUNETについてまとめる その6

本日は HoloToolKit の調査枠です。
SharingWithUNET のサンプルシーンを確認していきます。
bluebirdofoz.hatenablog.com


今回は UNETAnchorManager オブジェクトにある UNetAnchorManager について機能を確認します。
f:id:bluebirdofoz:20180111012642j:plain

UNetAnchorManager はワールドアンカーの作成・受取、これを用いた共通座標の設定を行います。

サーバの場合、本クラスの CreateAnchor() が呼び出され、アンカーを設定するのに適した位置を見つけます。
アンカーを作成したら、これをシリアル化し、GenericNetworkTransmitter クラスを通してクライアントに送信します。

クライアントの場合、GenericNetworkTransmitter クラスの受信イベントでアンカーデータをキャッチします。
アンカーデータをデシリアライズし、これをWorldAnchorManagerに記録します。

Anchor Name を同期することで、記録したアンカーの内、全てのホストは同名のアンカーを利用することになります。
アンカーは SharedCollection のインスタンスを持つゲームオブジェクトに設定され、これが共通の座標となります。


自作アプリを組み込む際にはひとまずノータッチでも問題なさそうです。
ただ、本スクリプトと GenericNetworkTransmitter スクリプトはシェアリングのワールドアンカーの取り扱いをまとめたスクリプトですので、理解を深めて損はなさそうです。

Anchor Name

現在利用中のアンカーネームが設定される。本変数には SyncVar 属性が設定されている。
SyncVar は NetworkBehaviour クラスのメンバ変数に置くことができる属性。
これらの変数は、サーバーから準備状態にあるゲーム内のクライアントに値が同期化される。



アンカーの設定位置を決定するのは以下の FindAnchorPosition 関数です。
SharingWithUNET では spatialMapping から最も頂点密度が高いサーフェスを検出して、そこをアンカーに定めます。

        /// <summary>
        /// Finds a good position to set the anchor.
        /// 1. If we have an anchor stored in the player prefs/ anchor store, use that
        /// 2. If we don't have spatial mapping, just use where the object happens to be 
        /// 3. if we do have spatial mapping, anchor at a vertex dense portion of spatial mapping
        /// アンカーを設定するのに適した位置を見つけます。
        /// 1.アンカーがプレイヤーの環境設定/アンカーストアに格納されている場合は、それを使います。
        /// 2.空間マッピングがない場合は、オブジェクトがどこに存在するかを使用するだけです。
        /// 3.空間マッピングを行うと、空間マッピングの頂点密度の高い部分にアンカーする。
        /// </summary>
        private void FindAnchorPosition()
        {
            // 1. recover a stored anchor if we can
            // 1.可能であれば、格納されたアンカーを復元する
            if (PlayerPrefs.HasKey(SavedAnchorKey) && AttachToCachedAnchor(PlayerPrefs.GetString(SavedAnchorKey)))
            {
                // SavedAnchorNameのハッシュキーで検索し、キャッシュ済みのアンカーが存在する場合

                // キャッシュ済みのアンカー名を取得する。
                exportingAnchorName = PlayerPrefs.GetString(SavedAnchorKey);
                Debug.Log("found " + exportingAnchorName + " again");
                // アンカーをバイト配列にエクスポートする。
                ExportAnchor();
            }
            // 2. just use the current object position if we don't have access to spatial mapping
            // 2.空間マッピングにアクセスできない場合は、現在のオブジェクト位置を使用します。
            else if (spatialMapping == null)
            {
                Debug.Log("No spatial mapping...");
                // SharedCollectionのインスタンスを持つゲームオブジェクトのワールド位置にアンカーをエクスポートします。
                ExportAnchorAtPosition(objectToAnchor.transform.position);
            }
            // 3. seek a vertex dense portion of spatial mapping
            // 3. 空間マッピングの頂点密集部分を求める。
            else
            {
                // SpatialMappingで保持されている全てのsurfaceObject(サーフェス)を取得する。
                ReadOnlyCollection<SpatialMappingSource.SurfaceObject> surfaces = spatialMapping.GetSurfaceObjects();
                if (surfaces == null || surfaces.Count == 0)
                {
                    // SpatialMapping内にsurfaceObject(サーフェス)が見つからなかった場合

                    // If we aren't getting surfaces we may need to start the observer.
                    // サーフェスを取得していない場合は、オブザーバを開始する必要があります。
                    if (spatialMapping.IsObserverRunning() == false)
                    {
                        // オブザーバ開始していない場合

                        // spatialMappingのオブザーバを開始する。
                        spatialMapping.StartObserver();
                        // spatialMappingのオブザーバフラグをtrueにする。
                        StartedObserver = true;
                    }

                    // And try again after the observer has a chance to get an update.
                    // オブザーバが更新した後、再度試してください。

                    // spatialMappingの更新間隔時間経過後、再びFindAnchorPositionを実行する。
                    Invoke("FindAnchorPosition", spatialMapping.GetComponent<SpatialMappingObserver>().TimeBetweenUpdates);
                }
                else
                {
                    // SpatialMapping内にsurfaceObject(サーフェス)が見つかった場合

                    // スタート時からの経過時間を記録しておく
                    float startTime = Time.realtimeSinceStartup;
                    // If we have surfaces, we need to iterate through them to find a dense area
                    // of geometry, which should provide a good spot for an anchor.
                    // サーフェスがある場合は、アンカーに適した場所になるジオメトリの密な領域を見つけるために、
                    // 反復処理を行う必要があります。
                    Mesh bestMesh = null;
                    MeshFilter bestFilter = null;
                    int mostVerts = 0;

                    for (int index = 0; index < surfaces.Count; index++)
                    {
                        // If the current surface doesn't have a filter or a mesh, skip to the next one
                        // This happens as a surface is being processed.  We need to track both the mesh 
                        // and the filter because the mesh has the verts in local space and the filter has the transform to 
                        // world space.
                        // 現在のサーフェスにフィルタまたはメッシュがない場合は、次のフィルタにスキップします。
                        // フィルタまたはメッシュがないサーフェスはサーフェスの処理中に発生します。
                        // メッシュにはローカル空間に頂点数があり、フィルタにはワールド座標への変換があるため、
                        // メッシュとフィルタの両方を追跡する必要があります。

                        // フィルタを取得します。
                        MeshFilter currentFilter = surfaces[index].Filter;
                        if (currentFilter == null)
                        {
                            // フィルタがない場合
                            continue;
                        }

                        // メッシュを取得します。
                        Mesh currentMesh = currentFilter.sharedMesh;
                        if (currentMesh == null)
                        {
                            // メッシュがない場合
                            continue;
                        }

                        // If we have a collider we can use the extents to estimate the volume.
                        // サーフェスがコライダーを持っていれば、
                        // エクステントを使ってサーフェスの大きさ(ベクトルの長さ)を見積もることができます。
                        MeshCollider currentCollider = surfaces[index].Collider;
                        float volume = currentCollider == null ? 1.0f : currentCollider.bounds.extents.magnitude;

                        // get th verts divided by the volume if any
                        // もしコライダ―があれば、その大きさで割ったメッシュの頂点数(verts)を取得します。
                        // 1.0fベクトル辺りの頂点数を取得することになります。
                        int meshVerts = (int)(currentMesh.vertexCount / volume);

                        // and if this is most verts/volume we've seen, record this mesh as the current best.
                        // 1.0fベクトル辺りの頂点数が最も多い場合は、このメッシュを現在のベストメッシュとして記録します。
                        mostVerts = Mathf.Max(meshVerts, mostVerts);
                        if (mostVerts == meshVerts)
                        {
                            // ベストメッシュの場合、メッシュとフィルタを記録する。
                            bestMesh = currentMesh;
                            bestFilter = currentFilter;
                        }
                    }

                    // If we have a good area to use, then use it.
                    // 使用する領域が十分あれば、それを使用してください。
                    if (bestMesh != null && mostVerts > 100)
                    {
                        // ベストメッシュが持つ頂点数が100を超える場合

                        // Get the average of the vertices
                        // メッシュに含まれる全ての頂点の平均値を取得する。
                        Vector3[] verts = bestMesh.vertices;
                        Vector3 avgVert = verts.Average();

                        // transform the average into world space.
                        // 取得した平均値をローカル座標からワールド座標へTransformPointで変換する。
                        // 計算したワールド座標位置が最も頂点密度の高い座標になる。
                        Vector3 center = bestFilter.transform.TransformPoint(avgVert);

                        Debug.LogFormat("found a good mesh mostVerts = {0} processed {1} meshes in {2} ms", mostVerts, surfaces.Count, 1000 * (Time.realtimeSinceStartup - startTime));
                        // then export the anchor where we've calculated.
                        // 計算したアンカーをエクスポートします。
                        ExportAnchorAtPosition(center);
                    }
                    else
                    {
                        // ベストメッシュが持つ頂点数が100以下の場合

                        // If we didn't find a good mesh, try again a little later.
                        // 良いメッシュが見つからなかった場合はもう一度やり直してください。
                        Debug.LogFormat("Failed to find a good mesh mostVerts = {0} processed {1} meshes in {2} ms", mostVerts, surfaces.Count, 1000 * (Time.realtimeSinceStartup - startTime));

                        // spatialMappingの更新間隔時間経過後、再びFindAnchorPositionを実行する。
                        Invoke("FindAnchorPosition", spatialMapping.GetComponent<SpatialMappingObserver>().TimeBetweenUpdates);
                    }
                }
            }
        }