【Unity】UnpackされたモデルのTransformをプレハブで再現するEditor拡張

例えばこんな岩のモデルがあったとして
![RockModel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/8ce62aac-71a2-7d6b-25c4-5b82216ea91f.png)

親GameObjectにColliderを付与して、それがそこそこ良い感じになるようにTransformの値をコネコネしたものをプレハブ化していたとします。
![Rockプレハブ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/9ea126cc-e3fc-f579-ef75-904b4a643f15.png)


その後暫くしてから仕様追加等でプレハブを編集したんだけど、「とあるシーンでは編集が効いていない」なんて不具合報告を受けて調査をしたところ…
![モデルが直接シーンに置かれてる.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/c47002e5-8a58-002f-61a0-0cda5a1e7431.png)
**プレハブUnpackしてモデルだけ直接シーンに置かれてる。。。**

なんてこと、ありますよね。[^1]
[^1]:正確にはUnpackという操作をしなくてもこの状態を作り出すことはできます。
実際はモデル部分のみをコピーして複製されてしまったというパターンの方が多いと思います。

このようなものは同じ見た目になるようにプレハブで再現して修復する必要があるわけですが、プレハブ内のモデルのTransformに複雑な値が入っていると、手作業で復元するのは至難の業です。

ということで、復元してくれるエディタ拡張を作りました。

## 使い方

復元対象のプレハブを拡張機能から判別できるように、専用のフォルダに一旦移動します。
![プレハブを専用フォルダに移動.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/0d99c2f3-055c-a71d-caf8-4af379d05c82.png)

Unpackされたモデルを右クリックしてメニューを選択すると
![拡張機能のメニュー選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/5fcbfe9e-83f3-053e-4fab-b9f1299aa059.png)

同じ見た目となるようにルートGameObjectのPosition, Rotation, Scaleを調整したプレハブがヒエラルキーに生成されます。
![再現完了.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/e3ca72d1-24bf-64a0-38e7-ba2a2170cb79.png)


## ソースコード

プレハブのルートGameObject直下の階層にモデルが配置されている前提の作りとなっています。
階層構造のあり方、復元対象のプレハブの指定方法等は利用場面に合わせて都度改変するのが良いと思います。

<details><summary>ソースコード</summary>

```cs
using UnityEditor;
using UnityEngine;

/// <summary>
/// UnpackしたGameObjectと同じPosition,Rotation,Scaleのものを再現したプレハブをヒエラルキーに配置する拡張機能
/// </summary>
/// <remarks>対象GameObjectを内包するプレハブをResources/<see cref="FolderName"/>フォルダに移動してから実行する必要があります。</remarks>
public static class UnpackedGameObjectRecreation
{
    private const string FolderName = "RecreationTarget";

    [MenuItem("GameObject/UnpackしたGameObjectをプレハブで再作成", false, 1)]
    public static void RecreateUnpackedGameObjectUsingPrefab()
    {
        var prefab = Resources.LoadAll<GameObject>(FolderName)[0];
        if (prefab == null)
        {
            Debug.LogError($"再現するプレハブをResources/{FolderName}フォルダに移動してから実行して下さい。");
            return;
        }

        foreach (var srcModel in Selection.transforms)
        {
            var createdRoot = (PrefabUtility.InstantiatePrefab(prefab) as GameObject).transform;
            var createdModel = createdRoot.GetChild(0);

            // 選択中のGameObjectと同じPosition, Rotation, Scaleとなるように、プレハブのルートGameObjectのTransformを変更
            var couldAdjust = AdjustScale(srcModel, createdModel);
            if (!couldAdjust)
            {
                UnityEngine.GameObject.DestroyImmediate(createdRoot.gameObject);
                continue;
            }

            AdjustRotation(srcModel, createdModel);
            AdjustPosition(srcModel, createdModel);
        }
    }

    private static bool AdjustScale(Transform src, Transform dest)
    {
        if (src.lossyScale == dest.lossyScale) return true;

        var tolerance = 0.0001f;
        var multiplier = src.lossyScale.x / dest.localScale.x;
        if (Mathf.Abs(multiplier - src.lossyScale.y / dest.localScale.y) > tolerance || Mathf.Abs(multiplier - src.lossyScale.z / dest.localScale.z) > tolerance)
        {
            Debug.LogError("選択したGameObjectとプレハブの子階層のGameObjectでScaleのXYZの比率が異なるため、再現できません。");
            return false;
        }

        dest.parent.localScale = Vector3.one * multiplier;
        return true;
    }

    private static void AdjustRotation(Transform src, Transform dest)
    {
        if (src.rotation == dest.rotation) return;

        dest.parent.rotation = src.rotation * Quaternion.Inverse(dest.localRotation);
    }

    private static void AdjustPosition(Transform src, Transform dest)
    {
        if (src.position == dest.position) return;

        dest.parent.position += src.position - dest.position;
    }
}
```

</details>

## 制限事項
### 複数選択対応

複数のGameObjectが選択されていたら全てのGameObjectをプレハブで再現するようになっています。

…が、[Unityの不具合](https://issuetracker.unity3d.com/issues/menuitem-is-executed-more-than-once-when-multiple-objects-are-selected)により、選択個数と同じ回数`MenuItem`の処理が呼び出されるので、同じGameObjectが選択個数分生成されてしまいます。(Unity2021.2.14にて確認)

#### ＜例＞
- 2個選択
    - → 2種類 x 2回処理 = 全部で4個生成される
- 3個選択
    - → 3種類 x 3回処理 = 全部で9個生成される  
      ![3x3個生成される.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1605307/027b63d4-2f54-cc1d-811b-113144adc78a.png)

これ修正しないって表明されていますが、なんでそんな判断になったのか疑問…。

コンテキストメニューからではなく、メニューバーの`[GameObject]`内から選択して実行すれば意図通りの挙動となります。

### Non-Uniform Scaling
[UnityのTransformのマニュアル](https://docs.unity3d.com/ja/2022.1/Manual/class-Transform.html)に記載のある通り、Non-Uniform Scaling（不均等スケーリング）と呼ばれる、ScaleのXYZ値が同一でないGameObjectの子オブジェクトは、親オブジェクトの軸でのみ拡縮可能となり、子オブジェクトの軸で拡縮させることができません。
このため、選択したGameObjectと、再現対象のプレハブの子階層のGameObjectでScaleのXYZ値の比率が同一でない場合は、親GameObjectのScaleのXYZを同一値で保ったまま再現することが不可能なので、エラーメッセージをコンソールに表示するようにしています。

Non-Uniform Scalingについては以下の記事にまとめました。

https://qiita.com/masamin/items/c7a23b798bb45bc266f8