ScriptedImporter（AssetImporter）のRemap

# TL;DR;

`ModelImporter`（FBXのインポーター）のインスペクタについている、マテリアル差し替えを実装できるものです。

![コメント 2019-10-25 075822.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/08149b22-9ac7-f555-cfee-1e9df540259c.png)


# Map？

AssetImporterの定義を眺めていると、`AssetImporter.Remap`なる関数がありました。なんだこれ？と思って調べてみるものの情報が乏しいかったのでメモ。（公式リファレンスにはFBXみたいなのだよとだけ書かれていています）
Mapという名詞が一般的過ぎて…ｗ

アセットをUnityのProjectViewにD&Dすると、各アセットごとに`AssetImporter`のインスタンスが生成され関連付けれれます。`AssetImporter`は`ModelImporter`、`TextureImporter`、そして`ScriptedImporter`などの基底クラスです。この`AssetImporter`君はExternalObjectMapという、辞書領域を持っています。この領域は、`AssetImporter`の外部から（例えばカスタムインスペクタ）から他のアセットファイルのオブジェクトを登録し、元のAssetImporterから参照できるようにするためのもののようです。

FBXのインポーター（`ModelImporter`）のように、ファイル内から生成されるべきサブアセットを上書きするのは典型的な利用例です。外部から参照を渡せる領域に過ぎないので、この他にも色々な利用法が考えられるでしょう。

ここでは、`ModelImporter`のような、Materialを上書きできるような実装を例に説明します。

```cs:CustomImporter.cs
[ScriptedImporter(1, "custom")]
public class CustomImporter: ScriptedImporter
{
    [Serializable]
    class Data
    {
        public List<Vector3> vertices;
        public List<int> trianles;
        public Color color;
    }

    public override void OnImportAsset(AssetImportContext ctx)
    {
        var data = JsonUtility.FromJson<Data>(File.ReadAllText(ctx.assetPath));

        var customObj = new GameObject();
        var mf = customObj.AddComponent<MeshFilter>();
        var mr = customObj.AddComponent<MeshRenderer>();
        ctx.AddObjectToAsset("CustomObject", customObj);

        var mesh = new Mesh();
        mesh.SetVertices(data.vertices);
        mesh.SetTriangles(data.trianles, 0);
        mf.mesh = mesh;
        ctx.AddObjectToAsset("CustomMesh", mesh);
        
        var material = new Material(Shader.Find("Standard"));
        material.color = data.color;
        mr.material = material;
        ctx.AddObjectToAsset("CustomMaterial", material);

        // Mapハッシュに含まれていれば取得して上書き
        if(GetExternalObjectMap().TryGetValue(
            new SourceAssetIdentifier(typeof(Material), "CustomMaterial"),
            out Object map)) 
        {
            if(map is Material)
            {
                mr.material = map as Material;
            }
        }

        ctx.SetMainObject(cube);
    }
}
```

肝は、`GetExternalObjectMap()`でマップ領域の参照を持ってきているところです。
上記の実装では、リインポートするたびにサブアセットのIDが変わってしまうので、2回目以降は前回のインポート結果を使いまわす処理が必要です。
https://gist.github.com/tsubaki/e7280fe3f02665089d6499669b7483d4

```json:Test.custom
{
    "vertices":
    [
        {"x": -1.0, "y":  0.0, "z":  0.0},
        {"x":  0.0, "y":  0.1, "z":  0.0},
        {"x":  1.0, "y":  0.0, "z": -1.0},

        {"x": -2.0, "y":  1.0, "z":  1.0},
        {"x":  1.0, "y":  0.2, "z":  1.0},
        {"x":  2.0, "y":  1.0, "z": -2.0}
    ],
    "trianles":
    [
        0, 1, 2, 3, 4, 5
    ]
    "color": {"r": 0.0, "g": 1.0, "b": 0.0}
}
```

このJSONファイル（拡張子は`.custom`）をUnity に読み込むと、2枚のポリゴンが緑に塗られたアセットがインポートされます。メッシュとマテリアルはサブアセットになります。

```cs
// assetPathのアセットのマテリアルをinjectMaterialで上書きします。
// もちろん元のファイルは不変です。
// インスペクタでラッピングしてあげれば、FBXのインポーターと同様になります。
static void RemapMaterial(string assetPath, Material inejectMaterial)
{
    AssetImporter.GetAtPath(assetPath).AddRemap(
        new AssetImporter.SourceAssetIdentifier(typeof(Material), "CustomMaterial"),
        injectMaterial);
}
```

injectMaterialに、紫色のマテリアルを与えて実行すると、`Test.cutom`のアセットのサブアセットのマテリアルは緑のまま、親GameObjectアセットの色が紫になるはずです。