UnityでBlenderの移動と回転の操作方法を再現する方法

Unityの操作にモヤモヤしていませんか?──Blender風トランスフォームツールを作りました

UnityのSceneビューでの移動・回転、もっと直感的にできたらいいのに…そう思ったことはありませんか?

  • 「Gキーでサクッと移動したい」
  • 「X/Y/Zキーで軸固定して作業したい」
  • 「視点を1/3/7キーで切り替えたい」
  • 「操作中にマウスで直感的にドラッグ移動したい」
  • 「Blenderのように操作できればなぁ…」

Blenderの操作感をUnity上で再現するEditorToolを作ってみました


主な特徴

Gキーで即座に移動モードへ
Rキーで回転モードに切り替え
X/Y/Zキーで移動・回転軸を即指定
1・3・7キーで視点変更(テンキー対応)
操作中のドラッグでマウスに追従する直感的なオブジェクト移動
カメラの向きに応じて動き方も自動補正(例:上からの視点でも直感的に動く)


なぜ作ったのか?

Unity標準の操作は柔軟ではありますが、
G → X → マウスでズバッと動かす
というあの快感がない。
モードレスで操作しようとすると、どうしても細かい位置合わせが面倒になります。

このツールは、手を止めず、視点を切り替えながら、キーボードとマウスだけで爆速編集できるようにすることを目的としています。


導入方法

GitHubやUnityパッケージマネージャーなどでの公開を予定しています。
現時点ではスクリプト1本をEditorフォルダに入れるだけで使えます。


最後に

このツールは、Blenderのヘビーユーザ向けのUnityツールです。
興味がある方は使ってみてください。

using UnityEngine;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEditor.ShortcutManagement;
using System.Collections.Generic;

[EditorTool("Blender-Like Transform Tool")]
public class BlenderLikeTransformTool : EditorTool
{
    private enum Mode { None, Move, Rotate }
    private Mode currentMode = Mode.None;
    private Vector3 axisConstraint = Vector3.one;
    private Dictionary<Transform, Vector3> startPositions = new();
    private Dictionary<Transform, Quaternion> startRotations = new();
    private Vector2 startMousePos;

    public override void OnToolGUI(EditorWindow window)
    {
        if (!(window is SceneView sceneView)) return;

        Event e = Event.current;
        Handles.BeginGUI();
        if (currentMode != Mode.None)
            GUI.Label(new Rect(10, 10, 300, 20), $"Mode: {currentMode} (Axis: {AxisName()})");
        Handles.EndGUI();

        if (e.type == EventType.KeyDown)
        {
            switch (e.keyCode)
            {
                case KeyCode.G:
                    EnterMoveMode(e.mousePosition); e.Use(); break;
                case KeyCode.R:
                    EnterRotateMode(e.mousePosition); e.Use();
                    axisConstraint = Vector3.zero;
                    break;
                case KeyCode.X:
                    axisConstraint = Vector3.right; e.Use(); break;
                case KeyCode.Y:
                    axisConstraint = Vector3.up; e.Use(); break;
                case KeyCode.Z:
                    axisConstraint = Vector3.forward; e.Use(); break;
                case KeyCode.Return:
                case KeyCode.KeypadEnter:
                case KeyCode.Mouse0:
                    Confirm(); e.Use(); break;
                case KeyCode.Escape:
                case KeyCode.Mouse1:
                    Cancel(); e.Use(); break;
                case KeyCode.Alpha1:
                    SetSceneViewDirection(e.control ? Vector3.forward : Vector3.back); e.Use(); break;
                case KeyCode.Keypad1:
                    SetSceneViewDirection(e.control ? Vector3.forward : Vector3.back); e.Use(); break;
                case KeyCode.Alpha3:
                    SetSceneViewDirection(e.control ? Vector3.left : Vector3.right); e.Use(); break;
                case KeyCode.Keypad3:
                    SetSceneViewDirection(e.control ? Vector3.left : Vector3.right); e.Use(); break;
                case KeyCode.Alpha7:
                    SetSceneViewDirection(e.control ? Vector3.up : Vector3.down); e.Use(); break;
                case KeyCode.Keypad7:
                    SetSceneViewDirection(e.control ? Vector3.up : Vector3.down); e.Use(); break;
                case KeyCode.Alpha5:
                    sceneView.orthographic = !sceneView.orthographic; sceneView.Repaint(); e.Use(); break;
                case KeyCode.Alpha9:
                    sceneView.rotation = Quaternion.Inverse(sceneView.rotation); sceneView.Repaint(); e.Use(); break;

            }
        }

        if (currentMode != Mode.None && Selection.transforms.Length > 0)
        {
            Vector2 delta = e.mousePosition - startMousePos;
            ApplyTransform(delta);
            SceneView.RepaintAll();

            if (e.type == EventType.MouseDown && e.button == 0)
            {
                Confirm(); e.Use();
            }
            if (e.type == EventType.MouseDown && e.button == 1)
            {
                Cancel(); e.Use();
            }
        }
    }

    private void EnterMoveMode(Vector2 mousePos)
    {
        currentMode = Mode.Move;
        axisConstraint = Vector3.one;
        StoreInitialTransforms();
        startMousePos = mousePos;
        SceneView.RepaintAll();
    }

    private void EnterRotateMode(Vector2 mousePos)
    {
        currentMode = Mode.Rotate;
        axisConstraint = Vector3.one;
        StoreInitialTransforms();
        startMousePos = mousePos;
        SceneView.RepaintAll();
    }

    private void Confirm()
    {
        if (currentMode == Mode.None) return;
        Undo.RecordObjects(Selection.transforms, $"{currentMode} Confirmed");
        Clear();
    }

    private void Cancel()
    {
        if (currentMode == Mode.None) return;
        foreach (var t in Selection.transforms)
        {
            if (startPositions.TryGetValue(t, out Vector3 pos)) t.position = pos;
            if (startRotations.TryGetValue(t, out Quaternion rot)) t.rotation = rot;
        }
        Clear();
    }

    private void Clear()
    {
        currentMode = Mode.None;
        startPositions.Clear();
        startRotations.Clear();
        SceneView.RepaintAll();
    }

    private void StoreInitialTransforms()
    {
        startPositions.Clear();
        startRotations.Clear();
        foreach (var t in Selection.transforms)
        {
            startPositions[t] = t.position;
            startRotations[t] = t.rotation;
        }
    }

    private void ApplyTransform(Vector2 delta)
    {
        foreach (var t in Selection.transforms)
        {
            if (currentMode == Mode.Move)
            {
                SceneView sceneView = SceneView.lastActiveSceneView;
                Camera cam = sceneView.camera;

                Ray ray1 = cam.ScreenPointToRay(startMousePos);
                Ray ray2 = cam.ScreenPointToRay(startMousePos + delta);

                Plane plane = new Plane(cam.transform.forward, startPositions[t]);

                if (plane.Raycast(ray1, out float enter1) && plane.Raycast(ray2, out float enter2))
                {
                    Vector3 p1 = ray1.GetPoint(enter1);
                    Vector3 p2 = ray2.GetPoint(enter2);
                    Vector3 move = p2 - p1;
                    move = Vector3.Scale(move, axisConstraint);
#if false
                    //t.position = startPositions[t] + new Vector3(move.x, -1 * move.y, move.z);
#else
                    Vector3 camUp = cam.transform.up;
                    // Y軸方向の符号判定:カメラが上を向いているなら +1、下を向いているなら -1
                    float ySign = Vector3.Dot(camUp, Vector3.up) > 0 ? -1f : 1f;
                    t.position = startPositions[t] + new Vector3(move.x, ySign * move.y,-1* ySign* move.z);
#endif
                }
            }
            else if (currentMode == Mode.Rotate)
            {
                Vector3 axis = axisConstraint;
                // カメラの向きを回転軸として使用
                SceneView sceneView = SceneView.lastActiveSceneView;
                Camera cam = sceneView.camera;
                if (axis == Vector3.zero) axis = cam.transform.forward.normalized;
                float angle = delta.x * 0.5f;
                t.rotation = Quaternion.AngleAxis(angle, axis) * startRotations[t];
            }
        }
    }

    private void SetSceneViewDirection(Vector3 dir)
    {
        SceneView sceneView = SceneView.lastActiveSceneView;
        if (sceneView == null) return;
        sceneView.LookAt(sceneView.pivot, Quaternion.LookRotation(dir), sceneView.size);
        sceneView.Repaint();
    }

    private string AxisName()
    {
        if (axisConstraint == Vector3.right) return "X";
        if (axisConstraint == Vector3.up) return "Y";
        if (axisConstraint == Vector3.forward) return "Z";
        return "Free";
    }

    [Shortcut("Activate Blender Tool", KeyCode.B)]
    private static void ActivateTool()
    {
        ToolManager.SetActiveTool<BlenderLikeTransformTool>();
    }
}

Unity

Posted by navy