🧰

ProBuilderのコードを読む (2) DrawShapeTool 1

2022/07/24に公開

EditorToolによる拡張方法

DrawShapeToolの概要

ProBuilderのツールバーでNew Shapeをクリックすると, エディタ上に指定した形状を描くことができます.

形状については別個スクリーン上に表示されるオーバーレイ画面で切り替えることができます.

DrawShapeToolがアクティブな場合, マウスアイコンは矢印の先に黄色の四角と+が表示され, ほかのことはできなくなります.

この状態で底部と高さの2段階に分けて形状の表示を行います.

ツールの管理自体はUnityEditorの機能であるToolManagerEditorToolで行っています.

DrawShapeToolモードへの切り替え

ツールの切り替え処理については前回のツールの管理で説明しました. あるツールがアクティブになるとそれ以降ツールに関連する画面を表示するメソッドであるOnToolGUI呼び出されます.

状態マシンによる描画状態の管理

ShapeState

状態遷移のためのクラスです. いわゆるステート・パターンで実装されており状態を表す3つの具象クラスがあります.

名前からおおよそ想像はつくと思いますが以下のように遷移します.

状態マシンの初期化

m_CurrentStateは最初は当然ShapeState_InitShapeです.

ShapeState_InitShapeで起きること

  • 平面の計算
  • 平面とレイの交点の計算
  • マウス・ダウン時に次のステートへ遷移
  • Shiftキーが押されたらプレビュー形状の表示 (プレビュー・ショートカット)
  • ドット・ハンドルの描画

どこに描画するのか?

描画平面の取得

この手のアプリケーションのキモは空間の奥行を適切に設定することです. ユーザーは大体この辺という感覚を奥行に持っていますが, それを自然に指定するのは難しいことです.

ProBuilerの場合見えない平面を想定して, 奥行きの情報を得ています. ShapeStaet_InitShape.DoState()では平面の位置をいくつかの条件を元に算出しています.

FindBestPlaneAndBitangentがいい感じの平面を返してくれます.

非常に大雑把に説明すると以下のような処理をしています.

いくつかの仮定を元に直観的な操作が可能なようになっています.

  1. シーン上にオブジェクトがある場合はその交点を使う
  2. UnityEditor標準のグリッドを使う
  3. ProGridがある場合はそのグリッドを使う

ProBuilderの用途からは1の場合が使いやすそうです. 普通平面とかをまず最初に置いてからマップを作成するはずなので.

交点を求める

平面が定まったらあとはレイキャストで交点を算出します.

if(res.item1.Raycast(ray, out hit))
{
    // 交点を求める
    m_HitPosition = tool.GetPoint(ray.GetPoint(hit));
    
    // マウスが押された場合だけ次の状態へと移行する.
    if(evt.type == EventType.MouseDown)
    {
        // バウンディング・ボックスの初期化
        return NextState();
    }
}
else
{
    m_HitPosition = Vector3.negativeInfinity;
}

マウスが押されるまでは交点の計算を繰り返します. そしてプラス記号付きのマウスカーソルとドット・ハンドルがスクリーン上に表示されます. Ready-to-Drawな状態です.

マウスが押されるとそこがスタート地点となって次の状態へ遷移します.

プレビュー・ショートカット

Shiftを押すとプレビュー・ショートカット機能で, 形状が1回で表示されます. その代わりインタラクティブな操作はできません. 前回のパラメータ設定が踏襲されるので直前に書いた図形をコピーする用途には向いていると思います.

DrawShape.DoDuplicateShapePreviewHandleを参照のこと.

アイドル状態のハンドルの描画

マウスが押すまではアイドル状態でプラス記号付きのマウスが表示されます. この時表示されるのが四角の黄色いアイコンです.

usingとHandles.DrawingScope

DrawingScopeを用いることでハンドルの色や行列をusingステートメントのブロック内でのみ有効にすることができます.

using (new Handles.DrawingScope(EditorHandleDrawing.vertexSelectedColor)) {
    Handles.DotHandleCap(-1, m_HitPosition, Quaternion.identity,
        HandleUtility.GetHandleSize(m_HitPosition) * 0.05f, EventType.Repaint);
}

EditorHandleDrawing.vertexSelectedColorはUnity標準のハンドル・カラーを使うように設定されています.

-1がコントロールIDとして設定されるのはこのハンドルが感知されないようにするためと思われます. [1]マウスダウンされるまではマウスはシーン上を漂います.

まとめ

処理は複雑ですが, やってることの本筋はマウスカーソルに合わせてハンドルを表示しマウス・ダウンが起きるのを待っているわけです. Editor拡張で待ちの状態を表現するのに使えそうですね.

次は状況を整理つつ複数回に分けで形状を表示する処理について理解を深めたいと思います.

Miscellaneous

GUIUtility.hotControl

ホットコントロールは

The hot control is one that is temporarily active. When the user mousedown's on a button, it becomes hot.

と書いてあります. つまりマウス・ダウン時のみ有効になります.

DrawShapeTool.cs
if(GUIUtility.hotControl == 0)
    EditorGUIUtility.AddCursorRect(new Rect(0, 0, Screen.width, Screen.height), MouseCursor.ArrowPlus);

とあるのは何もボタンが押されてないということを表すようです. 試しにProBuilderツールの画面上でマウスをクリックしてシーンビューに移動するとプラス記号は表示されなくなります. ドット・ハンドルの位置もシーン外で止まってしまいます.

ProBuilderEditor外でもProBuilderのツールであることを表す?

ProBuilderのツールバーはEditorWindowを使って表示されます. 実際の操作はシーン画面上で行われますが, 何もしなければシーン画面はProBuilderの存在に気づきません.

特定のツール(例えばDrawShapeTool)が選択された場合にそのコントロールIDを保持しておきます.

DrawShapeTool.cs
//  OnToolGUI()
m_ControlID = GUIUtility.GetControlID(FocusType.Passive);
HandleUtility.AddDefaultControl(m_ControlID);

こうすることでシーンビューの状態でもDrawShapeTool用のコントロールIDが取得できるようになります. 例えはShapeState_InitShapeでは, 同じコントロールを指しているかを確認します.

ShapeState_InitShape.cs
// DoState()
if(evt.isMouse && HandleUtility.nearestControl == tool.controlID)

...と思っているんですがnearestControlのニュアンスと一致しない.

【Unity】【エディタ拡張】HandleCapがクリックされたことを取得する
【Unity】GUIの範囲外でもドラッグの効果を継続する

デフォルト・ツールを優先する

ビュー・ツールが選択されている場合はProBuilderのツールは無効になります. つまりカスタム・ツールの優先度は低く設定されています. これをしないとUnityEditorのツールが使えないことになるかもしれません.

DrawShapeTool.cs
public override void OnToolGUI(EditorWindow window)
{
    // ...
    var evt = Event.current;
    
    // デフォルト・ツール(ViewTool)の場合は何もしない
    if (EditorHandleUtility.SceneViewInUse(evt))
        return;

    // DrawShapeTool状態のシーン画面の描画
}

SceneViewInUseはTools.viewToolActiveを呼び出して戻り値を返すだけです.

EditorHandleUtility.cs
public static bool SceneViewInUse(Event e) {
    return Tools.viewToolActive;
}

ViewToolとは

The View tool has options for Orbit, Pan, Zoom and FPS, ...

ですからシーン画面でのナビゲーション操作を指します. 処理の流れはこんな感じです.

イベントの順序

イベントの始まりはEventType.Layoutです.

This event is sent prior to anything else - this is a chance to perform any initialization.

そして終わりはEventType.Repaintです.

All other events are processed first, ...

この間に様々なイベントが処理されます.

Event各イベントに対してOnGUIが呼ばれるようです.

For each event OnGUI is called in the scripts; so OnGUI is potentially called multiple times per frame

[Event.current]を呼び出すということはあるOnGUIで処理されているイベントを取得することになるようです.

Further Reading

Reference

脚注
  1. これをしないとnearestControlがハンドルに設定されてしまうからマウスへ追従するハンドルではなくなってしまう? ↩︎

Discussion