mcpField
H
hz-unity-placement

hz-unity-placement

github.com/meta-quest開発241222 回インストール5 週間前に更新
リポジトリ

hz-unity-placement について

hz-unity-placementは、UnityのGameObjectを相対配置するための開発者向けスキルです。実行中のワールド空間の境界ボックスから位置を計算し、回転やスケール、親子関係を考慮して重なりを抑えます。

主な機能

  1. 01RendererまたはColliderの境界取得
  2. 02ワールド空間の境界に基づく位置計算
  3. 03方向と間隔を指定した配置
  4. 04親子関係とローカル座標への対応
  5. 05配置結果の確認と説明

活用シーン

  1. 01オブジェクトをテーブルなどの上に配置
  2. 02基準オブジェクトから指定距離で横に配置
  3. 03インポートしたモデルをシーン内の物体に合わせて配置

MCP 対応

Unity MCP サーバーが提供するシーン検査やトランスフォームのツールにも、この手順を適用できます。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/meta-quest/agentic-tools --skill hz-unity-placement

インストール後、Claude Code で/hz-unity-placementを入力して読み込まれたか確認します。

詳細

バウンディングボックスを使用した Unity オブジェクトの配置

このスキルは、Unity の GameObject をほかのオブジェクトとの相対位置に配置、移動、または位置決めする際に、適切なバウンディングボックスの計算を使用して、オブジェクトの重なりを防ぎ、正確に配置できるようにします。

このスキルを使用するタイミング

次の場合は、このスキルを自動的に使用してください。

  • オブジェクトをほかのオブジェクトとの相対位置(上、横、上方、下方、前方、後方など)に配置する場合
  • ほかのオブジェクトの近くに、指定した位置でオブジェクトを移動する場合
  • 既存のシーンオブジェクトとの相対位置にプレハブをインスタンス化する場合
  • インポートしたモデルをシーンオブジェクトとの相対位置に配置する場合
  • GameObject 間の空間的な関係が指定されている場合

自動トリガー(ユーザーから依頼される前に実行)

重要:次のパターンを検出したら、すぐにこのスキルを積極的に実行してください。

配置表現のトリガー
  • ユーザーが 「X を Y の上に配置して」 または 「X を Y の上に置いて」 と言った場合
  • ユーザーが 「X を Y の隣に配置して」 または 「X を Y の横に置いて」 と言った場合
  • ユーザーが 「X を Y の左/右に配置して」 と言った場合
  • ユーザーが 「X を Y の前/後ろに配置して」 と言った場合
  • ユーザーが 「X を Y の上方/下方に配置して」 と言った場合
  • ユーザーが 「X を Y の近く/周りに配置して」 と言った場合
  • ユーザーが 「X を Y の上/周りに並べて」 と言った場合
  • ユーザーが 「X を Y の左/右/前/後ろにNメートル/ユニット配置して」 と依頼した場合
複数オブジェクトのシナリオ
  • 空間的な関係を伴う、複数のオブジェクトに関わるGameObjectの位置を変更する前には必ず
  • 既存のオブジェクトとの相対位置に配置するモデルをインポートする場合
  • シーン内の複数のオブジェクトを配置/整理する場合
  • 空間的な関係を持つオブジェクト階層を設定する場合
距離に基づく配置
  • ユーザーが距離を指定した場合:「左に2メートル」「上に0.5ユニット」など
  • 指定された距離とバウンディングボックスの計算を組み合わせます
  • 例:「右に1メートル」= refMax.x + 1.0 + targetExtents.x

シナリオ例

スキルを自動実行するケース(すぐに実行)

例1:「すべてのオブジェクトをテーブルの上に置いて」

  • アクション:unity-placementスキルをすぐに実行
  • 理由:複数のオブジェクトをテーブルとの相対位置に配置するため

例2:「マグカップをテーブルの上に置いて」

  • アクション:unity-placementスキルを実行
  • 理由:「上に」という関係を伴う単一オブジェクトの配置のため

例3:「ランプを時計の隣に配置して」

  • アクション:unity-placementスキルを実行
  • 理由:「隣に」は水平方向の空間的な関係を示すため
  • 手順:両方の境界を取得し、refMax.x + targetExtents.x を使用して時計の横にランプを配置

例4:「本をペンの左に0.5メートルの位置に配置して」

  • アクション:unity-placementスキルを実行
  • 理由:空間的な関係を伴う、距離に基づく配置のため
  • 手順:境界を取得し、次のように計算:penMin.x - 0.5 - bookExtents.x
スキルを実行しないケース(直接操作)

例1:「オブジェクトを[5, 10, 3]に移動して」

  • 理由:直接座標が指定されており、相対位置への配置ではないため

例2:「キューブの位置を(0, 0, 0)に設定して」

  • 理由:絶対位置であり、参照オブジェクトがないため

基本原則

位置を計算する前に、必ずバウンディングボックスの情報を取得してください。

オブジェクトのサイズを推測してはいけません。実行中のEditor上で、RendererまたはColliderコンポーネントから実際の境界を必ず取得してください。

Editorの操作

.unity YAMLを編集するのではなく、unity-cliを通してシーンを読み書きしてください。生ファイルの編集内容は実行中のEditorから見えず、誤ったシーンを編集しやすいためです。

unity status --format json          # look for state "ready"

接続方法、--project-path、Safe Modeからの復旧については、**unity-cli**スキルを参照してください。Unity MCP サーバーを使用する場合も同じ手順が適用されます。公開されているシーンの検査ツールとTransformツールを使用してください。

手順

ステップ1:対象オブジェクトを特定する

ユーザーがオブジェクトの配置を依頼した場合:

  1. 対象オブジェクト(配置/移動するオブジェクト)を特定します
  2. 参照オブジェクト(その相対位置に配置するオブジェクト)を特定します
  3. 希望する空間的な関係(上、横、上方など)を確認します
ステップ 2:バウンディングボックス情報を取得する

両方のオブジェクトについて、位置を特定し、境界を読み取ります。

unity command find_gameobjects --name Cube --format json

bounds はシリアライズされたフィールドではなく計算されるプロパティのため、get_component_properties では返されません。MeshRenderer を読み取ると、m_Materials、m_CastShadows などは取得できますが、境界は取得できません。境界を読み取るには、常にスクリプトを使用します。

// AgentScripts/GetBounds.cs
using System.Text;
using UnityEngine;

public static class GetBounds
{
    public static string Run(string[] names)
    {
        var sb = new StringBuilder();
        foreach (var name in names)
        {
            var go = GameObject.Find(name);
            if (go == null) { sb.AppendLine($"{name}: NOT FOUND"); continue; }

            if (!TryGetBounds(go, out var b, out var src))
            {
                sb.AppendLine($"{name}: NO Renderer or Collider - bounds unknown");
                continue;
            }

            sb.AppendLine($"{name} [{src}] center={b.center} size={b.size} extents={b.extents} min={b.min} max={b.max}");
        }
        return sb.ToString();
    }

    // Root renderer/collider first; otherwise the union of the children's, which is what an
    // imported model needs since its renderers sit on child GameObjects.
    private static bool TryGetBounds(GameObject go, out Bounds bounds, out string source)
    {
        bounds = default;
        source = null;

        var r = go.GetComponent<Renderer>();
        if (r != null) { bounds = r.bounds; source = "Renderer"; return true; }

        var col = go.GetComponent<Collider>();
        if (col != null) { bounds = col.bounds; source = "Collider"; return true; }

        foreach (var cr in go.GetComponentsInChildren<Renderer>())
        {
            if (source == null) { bounds = cr.bounds; source = "child Renderers"; }
            else bounds.Encapsulate(cr.bounds);
        }
        if (source != null) return true;

        foreach (var cc in go.GetComponentsInChildren<Collider>())
        {
            if (source == null) { bounds = cc.bounds; source = "child Colliders"; }
            else bounds.Encapsulate(cc.bounds);
        }
        return source != null;
    }
}
unity command run_script --file AgentScripts/GetBounds.cs --entry GetBounds.Run \
  --args '[["Cube","Table"]]' --format json

--args は JSON 配列を位置パラメーターとして展開するため、string[] パラメーターを 1 つだけ渡す場合は、配列を入れ子にする必要があります。'["Cube","Table"]' は 2 つの引数として読み取られるため、"expects 1 argument(s) but 2 were provided" というエラーになります。

Unity における境界の種類について:

プロパティ空間回転を考慮するか使用する場面
Renderer.boundsワールドはい(回転したメッシュを囲む AABB)シーン内にオブジェクトを配置する場合 — 配置にはこちらを推奨
Renderer.localBoundsローカルいいえ(回転を無視)回転の影響を受けずに、オブジェクト本来のサイズを比較する場合
Collider.boundsワールドはい(回転したコライダーを囲む AABB)Renderer が存在しないオブジェクトを配置する場合

配置には、常にワールド空間の境界(Renderer.bounds または Collider.bounds)を優先してください。これらはオブジェクトの位置、回転、スケールをすでに考慮しているため、手動で変換する必要はありません。

ステップ 3:ワールド境界をそのまま使用する(またはローカルから変換する)

ワールド空間の境界を使用する場合(推奨 — Renderer.bounds または Collider.bounds から取得):

値はそのまま使用できます。

worldCenter = bounds.center    // already in world space
worldMin = bounds.min          // already in world space
worldMax = bounds.max          // already in world space
size = bounds.size             // world-space AABB dimensions
extents = bounds.extents       // half of size

ローカル空間の境界を使用する場合(Renderer.localBounds または手動で取得したコンポーネントデータから取得):

オブジェクトの位置を加算してワールド空間に変換します。

worldCenter = position + localBounds.center
worldMin = worldCenter - (localBounds.size / 2)
worldMax = worldCenter + (localBounds.size / 2)

回転に関する注意: ローカル境界は回転を無視します。回転したオブジェクトのローカル境界を変換すると、算出された AABB は実際のワールド空間での占有範囲を反映しません。可能な限り、ワールド空間の境界を優先してください。

各オブジェクトについて、次の値を保存します。

  • worldCenter:[x, y, z]
  • size:[幅, 高さ, 奥行き]
  • extents:[幅/2, 高さ/2, 奥行き/2]
ステップ 4:配置位置を計算する

空間的な関係に基づいて、目標位置を計算します。

上に配置(オブジェクト A をオブジェクト B の上に配置):

targetY = B.worldMax.y + A.extents.y
targetX = B.worldCenter.x
targetZ = B.worldCenter.z
newPosition = [targetX, targetY, targetZ]

横に配置(オブジェクト A をオブジェクト B の横、+X 方向に配置):

targetX = B.worldMax.x + A.extents.x
targetY = B.worldCenter.y
targetZ = B.worldCenter.z
newPosition = [targetX, targetY, targetZ]

前に配置(オブジェクト A をオブジェクト B の前、+Z 方向に配置):

targetX = B.worldCenter.x
targetY = B.worldCenter.y
targetZ = B.worldMax.z + A.extents.z
newPosition = [targetX, targetY, targetZ]

上方に配置(間隔を空けて上方に浮かせる):

gap = 0.5  // or specified distance
targetY = B.worldMax.y + gap + A.extents.y
targetX = B.worldCenter.x
targetZ = B.worldCenter.z
newPosition = [targetX, targetY, targetZ]

隣または横に、間隔を空けて配置(オブジェクト A をオブジェクト B の隣に、指定した間隔を空けて配置):

gap = user_specified_distance  // e.g., 0.5 meters
// Right side (+X):
targetX = B.worldMax.x + gap + A.extents.x
// Left side (-X):
targetX = B.worldMin.x - gap - A.extents.x
targetY = B.worldCenter.y  // or B.worldMin.y + A.extents.y for ground level
targetZ = B.worldCenter.z
newPosition = [targetX, targetY, targetZ]

左/右/前/後に X メートル/ユニット離して配置:

// "2 meters to the right of B"
targetX = B.worldMax.x + 2.0 + A.extents.x

// "1.5 units to the left of B"
targetX = B.worldMin.x - 1.5 - A.extents.x

// "0.5 meters in front of B"
targetZ = B.worldMax.z + 0.5 + A.extents.z

// "1 unit behind B"
targetZ = B.worldMin.z - 1.0 - A.extents.z
ステップ 5:位置を適用する

set_transform が設定するのはワールド位置ではなく localPosition です。 検証済み:ワールド x=10 の親オブジェクトの子に対して、--position "[1,0,0]" を指定すると、localPosition (1,0,0) / position (11,0,0) になります。上記のすべての計算式で求められるのはワールド位置なので、次のようにします。

  • 親オブジェクトがない場合 — ローカル位置とワールド位置は同じなので、計算値をそのまま適用します。

    unity command set_transform --target Cube --position "[0,1.05,2]" --format json
    unity command save_scene --format json
    

    配列パラメーターは、1 つの引数として JSON 配列を指定します。--position 0 1.05 2 は INVALID_COMMAND_ARGS で拒否されます。

  • 親オブジェクトがある場合 — 先に変換するか、ワールド空間の位置であることが明確なスクリプトで transform.position を設定します。

    // AgentScripts/SetWorldPosition.cs
    using UnityEditor;
    using UnityEditor.SceneManagement;
    using UnityEngine;
    
    public static class SetWorldPosition
    {
        public static string Run(string name, float x, float y, float z)
        {
            var go = GameObject.Find(name);
            if (go == null) return $"ERROR: '{name}' not found.";
    
            Undo.RecordObject(go.transform, "Place object");
            go.transform.position = new Vector3(x, y, z);
            EditorSceneManager.MarkSceneDirty(go.scene);
            return $"{name}: world={go.transform.position} local={go.transform.localPosition}";
        }
    }
    
    unity command run_script --file AgentScripts/SetWorldPosition.cs \
      --entry SetWorldPosition.Run --args '["Cube", 0, 1.05, 2]' --format json
    unity command save_scene --format json
    

スクリプトは両方の空間の値を返します。配置された位置を確認するには、これが唯一の方法です。get_component_properties --type Transform で取得できるのは m_LocalPosition のみのため、ワールド位置かローカル位置かを判別できません。

ステップ 6:配置を確認する

配置後、ユーザーに次の内容を伝えます。

  • 計算した位置
  • 使用した境界
  • 行った調整
  • Scene ビューで確認するよう提案します。ヘッドセットで実行中の場合は、metavr capture screenshot で確認できます。

よくある配置パターン

上に配置(積み重ね)
  • 基準オブジェクトの上面(worldMax.y)を使用します
  • 対象オブジェクトの半分の高さ(extents.y)を加算します
  • 水平方向の中心を揃えます(X、Z を一致させます)
横に隣接
  • 参照オブジェクトの側面(worldMax.x または worldMin.x)を使用します
  • ターゲットオブジェクトの半幅(extents.x)を加えます
  • 中心を垂直方向(Y が一致)と奥行き方向(Z が一致)で揃えます
前/後ろ
  • 参照オブジェクトの前面/背面(worldMax.z または worldMin.z)を使用します
  • ターゲットオブジェクトの半奥行き(extents.z)を加えます
  • 中心を揃えます(X、Y が一致)
特定のオフセット位置
  • 参照オブジェクトの中心から始めます
  • カスタムオフセットを加えます
  • 適切に接地させるため、ターゲットオブジェクトの extents も引き続き考慮します

ベストプラクティス

  1. 常に最新の bounds を取得する:bounds をキャッシュしないでください。配置前に必ず取得します
  2. スケールを考慮する:ワールド空間の bounds には、すでにオブジェクトのスケールが反映されています
  3. 回転を考慮する:ワールド空間の bounds(Renderer.bounds)は、回転したメッシュを囲むように拡張される軸平行境界ボックス(AABB)です。Z 軸を中心に 45 度回転した 1x0.1x1 の平面は、平らなときよりも高い AABB になります。これは正しい動作です。AABB はオブジェクトが実際に占める空間を反映します。回転が自動的に処理されるよう、配置には常にワールド空間の bounds を使用します
  4. Renderer がない場合に対処する:Renderer がない場合は Collider を確認し、どちらもない場合はユーザーに警告します
  5. 計算を説明する:計算過程を示し、見つかった bounds と位置の算出方法をユーザーに伝えます
  6. ローカル位置とワールド位置:set_transform はローカル位置を書き込みます。親がないオブジェクトではワールド位置と同じですが、親がある場合は異なります。そのため、parent.InverseTransformPoint(worldPos) で変換するか、run_script から transform.position を設定して、両方の値をユーザーに報告してください
  7. Prefab に対処する:Prefab をインスタンス化する場合は、インスタンス化後に bounds を取得します

エッジケースへの対処

オブジェクトに Renderer も Collider もない
  • bounds を特定できないことをユーザーに警告します
  • Collider を追加するよう提案するか、寸法を手動で指定するよう求めます
  • オブジェクトのピボット位置にサイズ 0 を仮定して代用します
複数の Renderer(子を持つ親)
  • 利用可能であれば、親の Renderer を使用します
  • 親に Renderer がない場合は、子から統合 bounds を計算します
  • このことをユーザーに伝えます
回転したオブジェクト
  • ワールド空間の bounds(Renderer.bounds)は、回転したジオメトリを囲むように拡張される軸平行境界ボックス(AABB)です
  • 回転したオブジェクトの AABB は通常、オブジェクトの見た目の輪郭より大きくなるため、オブジェクト同士をぴったり接して配置したときに、目に見える隙間ができることがあります
  • これは想定どおりの動作です。隙間が望ましくない場合はユーザーに警告し、回転を調整するか、手動オフセットを使うよう提案します
  • ユーザーが回転した表面にぴったり配置する必要がある場合は、AABB に頼るのではなく、オブジェクトのローカルの forward/up/right ベクトルと localBounds を使って表面上の点を計算することを検討してください
不規則な形状
  • bounds は軸平行のボックス(AABB)で、メッシュ全体を含む、ワールド軸に沿った最小のボックスを表します
  • 箱形でない形状(例:斜めの梁、AABB の角が空いている球体)では、目に見えるメッシュよりかなり大きくなる場合があります
  • 配置時に隙間ができた場合は、このことをユーザーに伝えます
非常に小さいオブジェクト、または非常に大きいオブジェクト
  • bounds が妥当と思われる値か確認します(size > 0)
  • extents が極端に小さい(< 0.01)または大きい(> 100)場合は警告します

ワークフローの例

ユーザー:「Cube を Sphere の上に配置して」

  1. Sphere のコンポーネントを取得し、bounds を抽出します:

    • サイズ:[2, 2, 2]、位置:[0, 1, 0]
    • ワールド中心:[0, 1, 0]、Extents:[1, 1, 1]
    • ワールド最大 Y:2
  2. Cube のコンポーネントを取得し、bounds を抽出します:

    • サイズ:[1, 1, 1]、現在の位置:[5, 0, 0]
    • Extents:[0.5, 0.5, 0.5]
  3. 新しい位置を計算します:

    • 目標 Y:2(Sphere の上端)+ 0.5(Cube の半高さ)= 2.5
    • 目標 X:0(Sphere の中心 X)
    • 目標 Z:0(Sphere の中心 Z)
    • 新しい位置:[0, 2.5, 0]
  4. Cube に位置を適用します

  5. 報告:「Cube を位置 [0, 2.5, 0] に配置しました。サイズ 1x1x1 の Cube は、Sphere の上面(Y=2)に接しています。」

クイックリファレンス

Unity の座標軸
  • X:右(+)/左(-)
  • Y:上(+)/下(-)
  • Z:前方(+)/後方(-)
一般的な Bounds プロパティ

ワールド空間(Renderer.bounds、Collider.bounds)— 配置に使用します:

  • bounds.center:AABB のワールド空間での中心
  • bounds.size:AABB の全体寸法(回転とスケールを考慮)
  • bounds.extents:半寸法(size / 2)
  • bounds.min:ワールド空間での最小コーナー
  • bounds.max:ワールド空間での最大コーナー

ローカル空間(Renderer.localBounds)— 固有サイズの比較に使用します:

  • localBounds.center:オブジェクトのピボットからのローカルオフセット
  • localBounds.size:固有寸法(回転を無視)
  • localBounds.extents:半寸法(size / 2)
配置の計算式
  • 上に配置:refMax.y + targetExtents.y
  • 下に配置:refMin.y - targetExtents.y
  • 右に配置:refMax.x + targetExtents.x
  • 左に配置:refMin.x - targetExtents.x
  • 前方に配置:refMax.z + targetExtents.z
  • 後方に配置:refMin.z - targetExtents.z
距離に基づく配置の計算式
  • 右に X 単位:refMax.x + distance + targetExtents.x
  • 左に X 単位:refMin.x - distance - targetExtents.x
  • 前方に X 単位:refMax.z + distance + targetExtents.z
  • 後方に X 単位:refMin.z - distance - targetExtents.z
  • 上に X 単位:refMax.y + distance + targetExtents.y
  • 下に X 単位:refMin.y - distance - targetExtents.y
  • 隙間を空けて隣に配置:refMax.x + gap + targetExtents.x(または左側には refMin.x を使用)

覚えておくこと

オブジェクトを直感的かつ正確に配置することが目的です。必ず次の点を守ってください:

  1. run_script と bounds reader を使って、実行中の Editor 上のコンポーネントから実際の bounds を取得します — get_component_properties では bounds を取得できません
  2. ワールド空間での位置を計算します
  3. オブジェクトの extents(半サイズ)を考慮します
  4. ローカル空間とワールド空間の違いに注意して適用します:set_transform はローカル空間で動作します
  5. 計算方法をユーザーに説明します
  6. オブジェクトのサイズを推測しません
  7. bounds を確認せずに、任意の位置にオブジェクトを配置しません

コメント