【Unity】ワールド座標とローカル座標で関節を回転させる ― 方向ベクトルを保つ「ツイスト」回転の実装

投稿日: 2026年6月6日
対象読者: Unityでボーン/関節を手動回転させるUIを作りたいエンジニア、デッサン人形・ポーズ調整ツールを実装したい方

この記事はこんな方向け:

  • 回転ボタンを押すと、世界の上下左右では動くが「腕の向きを変えずにひねりだけ加えたい」が実現できない方
  • transform.RotateSpace.WorldSpace.Self の違いを、実例で理解したい方
  • 回転の基準軸(ギズモ)をボーンに追従表示させたい方

TL;DR

  • ワールド回転は transform.Rotate(axis, angle, Space.World)、ローカル回転は Space.Self第3引数を切り替えるだけで挙動が変わる。
  • ローカル回転では axisボーンのローカル座標として解釈される。ボーンの「向き」をローカルY軸(up)に取っているリグなら、**Y軸回転は方向ベクトルを保ったままの「ツイスト(ひねり)」**になる。
  • 軸ギズモは Vector3.right/up/forward(ワールド固定)と target.right/up/forward(ボーン追従)を切り替えるだけで、実際に回る軸を可視化できる。
  • Pose Mirror では1つのトグルボタンでこの2モードを切り替えられるようにした。

はじめに:ワールド回転だけでは届かない調整

ボーンを手動で回す調整UIを作ると、最初はだいたい「X/Y/Zボタンで世界の軸まわりに回す」実装になります。これはこれで直感的なのですが、すぐにある壁にぶつかります。

たとえば腕をある方向へ向けたあと、「向きはそのままに、前腕のひねり(ロール)だけ少し足したい」。ところがワールドのY軸で回すと、腕全体が世界の鉛直軸を中心に振り回され、せっかく合わせた向き(方向ベクトル)まで動いてしまいます。

欲しいのは「方向ベクトルは変えず、その軸まわりに回転だけ加える」操作です。これはまさにローカル座標での回転であり、Unityでは Space.Self 一つで実現できます。本記事では、ワールド/ローカルをトグルで切り替えるUIを題材に、両者の違いと実装、そして「なぜローカルY回転がツイストになるのか」を解説します。

基礎:Space.WorldSpace.Self の違い

transform.Rotate は第3引数で「axis をどの座標系のベクトルとして解釈するか」を指定します。同じ「Y軸で回す」でも、基準の取り方で回転軸がまったく別物になります。

Space.WorldとSpace.Selfの違い:Worldでは軸が世界に固定されボーンが傾いてもYは常に真上なので回すと向きごと動く。Selfでは軸がボーンと一緒に傾き、Y軸がボーンの向きに一致するためY回転は向きを保ったままのツイストになる

これをコードで書き分けると次のとおりです。

// ワールド座標軸まわりに回す:axis は世界の向き
transform.Rotate(Vector3.up, angle, Space.World);

// 自分(ボーン)のローカル軸まわりに回す:axis はボーン基準の向き
transform.Rotate(Vector3.up, angle, Space.Self);
  • Space.WorldVector3.up は常に世界の鉛直方向。ボーンがどんな姿勢でも、回転軸は世界の上方向に固定される。
  • Space.SelfVector3.upそのボーンのローカルY軸。ボーンが傾いていれば回転軸も一緒に傾く。

この一点を押さえれば、あとは「どちらの Space を渡すか」を切り替えるだけです。

実装:1つのフラグでSpaceを切り替える

Pose Mirror の回転UIでは、X±/Y±/Z± のボタンがすべて共通の回転メソッドを呼びます。そこに「現在モード」を表す bool を1つ用意し、Space を出し分けます。

// false = ワールド座標軸で回転 / true = ボーンのローカル軸で回転
private bool localMode = false;

private void RotateBone(Vector3 axis, float angle)
{
    if (target == null) return;

    // ★ここが本質:モードに応じて解釈する座標系を切り替える
    Space space = localMode ? Space.Self : Space.World;

    target.Rotate(axis, angle, space);
}

ボタン側は軸ベクトルを渡すだけです。

public void RotateYPlus() => RotateBone(Vector3.up,    +stepAngle);
public void RotateXPlus() => RotateBone(Vector3.right, +stepAngle);
public void RotateZPlus() => RotateBone(Vector3.forward, +stepAngle);

切り替えはトグルボタンのクリックで行います。onClick はコードからリスナー登録しておくと、シーン側でUnityEventを手配線せずに済みます。

void Start()
{
    if (modeToggleButton != null)
        modeToggleButton.onClick.AddListener(ToggleRotationMode);
    if (modeLabel != null)
        modeLabel.text = localMode ? "Local" : "World";
}

public void ToggleRotationMode()
{
    localMode = !localMode;
    if (modeLabel != null) modeLabel.text = localMode ? "Local" : "World";
    if (axisGizmo != null)  axisGizmo.useLocalAxes = localMode; // ギズモ表示も連動(後述)
}

ワールド/ローカルを切り替えるトグルボタン(操作パネル上部の「World」。下に部位選択・X/Y/Z回転ボタンが並ぶ)

回転の基準軸(ギズモ)をボーンに追従させる

ローカルモードでは「いま実際に回る軸」がボーンと一緒に傾きます。これを目で確認できるよう、軸ギズモ(X=赤 / Y=緑 / Z=青 のライン)も同じフラグで切り替えます。やることは、軸ベクトルをワールド固定にするかボーン追従にするかの分岐だけです。

public bool useLocalAxes = false;

void LateUpdate()
{
    if (target == null) return;
    Vector3 origin = target.position;

    // useLocalAxes = true ならボーンのローカル軸、false ならワールド固定軸
    Vector3 ax = useLocalAxes ? target.right   : Vector3.right;
    Vector3 ay = useLocalAxes ? target.up      : Vector3.up;
    Vector3 az = useLocalAxes ? target.forward : Vector3.forward;

    xLine.SetPosition(0, origin); xLine.SetPosition(1, origin + ax * length);
    yLine.SetPosition(0, origin); yLine.SetPosition(1, origin + ay * length);
    zLine.SetPosition(0, origin); zLine.SetPosition(1, origin + az * length);
}

実際の見た目の違いがこちらです。

ワールドモード:緑(up)は常に真上を指し、ボーンの姿勢に関係なく軸は世界に固定されています。

ワールドモードの軸ギズモ:緑のupは常に世界の鉛直方向を指す

ローカルモード:軸がボーンの姿勢に合わせて傾き、ワールドでは奥(カメラ方向)に隠れていた青(forward)も見えるようになります。これが「ボーンのローカル座標系」です。

ローカルモードの軸ギズモ:赤・緑がボーンに合わせて傾き、青のforwardも現れる

なぜローカルY回転が「方向ベクトルを保つツイスト」になるのか

ポイントはリグの軸の取り方にあります。Pose Mirror では、ボーンの**「向き(方向ベクトル)」をローカルのY軸(up)**に対応させています(このupベクトルの算出には外積を使っています。詳細は オブジェクトをターゲット方向に向かせる方法 を参照)。

つまり、

  • ローカルY軸 = ボーンが指している方向そのもの

です。回転の基本性質として「回転軸上のベクトルは回転で動かない」ため、ローカルY軸(=方向ベクトル)まわりに回しても、ボーンが指す向きは変わりません。変わるのは軸まわりのロール(ひねり)だけ ― これがまさに求めていた「方向は保ったままのツイスト」です。

一方、ローカルのX軸やZ軸で回せば方向ベクトル自体が動くので、向きごと変えたいときに使えます。同じローカルモードでも、軸の選び方で「ひねり」と「向き変え」を使い分けられるわけです。

つまずきポイント(実体験メモ)

  • ボタンの onClick はコード側で AddListener。複製ボタンに残りがちな旧イベント(長押し連続回転など)を引きずらず、参照を [SerializeField] で1本に絞れるので確実。
  • 日本語ラベルが「□□□□」になる:UIフォント(LiberationSans系)に日本語グリフが無い環境では、World/ローカル のラベルが豆腐化する。Pose Mirror では他ボタン(-X/+X等)に合わせて**英字ラベル「World/Local」**に統一して回避した。日本語ラベルを使うなら日本語グリフを含むフォントアセットが必要。
  • Chestなど階層ボーンの追従回転を併用している場合も、Space 変数を各 Rotate 呼び出しに渡すだけで両モードに同時対応できる。

まとめ

ワールドとローカルの切り替えは、突き詰めれば transform.Rotate の第3引数を出し分けるだけです。実装はフラグ1つと分岐1行で済みます。

肝になるのは軸の取り方です。ボーンの方向ベクトルをローカルY軸に合わせておけば、ローカルY回転は「向きを保ったままのツイスト」になり、X/Z軸を選べば向きごと変えられます。あとはギズモを Vector3.* から target.* へ切り替えて「実際に回る軸」を見せてやれば、操作の直感性は一段上がります。

Pose Mirror(MediaPipeのポーズを3Dモデルへ反映するWebアプリ)でも、この切り替えで手動のポーズ微調整がぐっとやりやすくなりました。

ぜひ実際に Pose Mirror を動かして、ワールド/ローカルの切り替えを試してみてください!


関連記事:

参考資料

← ポートフォリオ TOP へ戻る