PNGなど既存AssetのImporterを上書きする SetImporterOverride

# 前書き

この記事は、2023のUnityアドカレの12/21の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい！

https://qiita.com/advent-calendar/2023/present-calendar#qiita_award

# はじめに

Unityには、任意の拡張子のファイルに対し、`ScriptedImporter`クラスを実装することで、そのファイルをUnityのアセットとして対応させることができます。以前には、これを使って、Shaderアセットを非標準のShader言語から作り出す方法を紹介しました。

https://qiita.com/up-hash/items/dc9c9c19bebefbe869cd

このScriptedImporterですが、Unity組み込みで対応している拡張子（PNGやmp3など）は定義することができません。

```csharp
[ScriptedImporter(1, "png")]
class MyPngImporter : ScriptedImporter
{
    ...
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/cc46aa59-7341-1239-cc98-0680610d4717.png)

しかし、これは本当に不可能なのでしょうか？（疑心暗鬼）

# PSDのインポータ

Unityは昔からPSDのインポートをサポートしています。それは、単純に全レイヤーを結合して一枚のTexture2Dとして取り込むという機能です。

しかし、追加パッケージにも`com.unity.2d.psdimporter`というものがあります。これは、レイヤー構造を認識してもっと賢く構造的にインポートをしてくれる代物です。

https://docs.unity3d.com/ja/Packages/com.unity.2d.psdimporter@9.0/manual/index.html

上記の理屈では、PSDはもう組み込みでサポートされてしまっているはずなので、独自のScriptedImporterで対応することができないはずです。

マニュアルを読んでみると…

> デフォルトでは、.psd ファイルのインポートにはテクスチャインポーターを使用します。PSD Importer を使用して .psd ファイルをインポートする場合は、.psd ファイルを選択し、Importer ドロップダウンをクリックして、UnityEditor.U2D.PSD.PSDImporter を選択します。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/481369fe-1841-9e66-fcf6-772a58180c41.png)

https://docs.unity3d.com/ja/Packages/com.unity.2d.psdimporter@9.0/manual/PSD-override.html

どうやらやっぱり対応しているようです。そしてその下に、`AssetDatabase.SetImporterOverride<TImporter>`を使うことで、スクリプトからもインポータを切り替えることができると書いてあります。

> このプロセスを自動化するスクリプトを作成することもできます。以下は、AssetDatabase.SetImporterOverride メソッドを使用して自動化スクリプトを作成する方法の例です。
> ```cs
> [MenuItem("Assets/2D Importer/Change PSD File Importer", false, 30)]
> static void ChangeImporter()
> {
>     ...// すべての.psdファイルをサーチ
>     AssetDatabase.SetImporterOverride<PSDImporter>(path);
> }
> ```

ということで、PSDImporterはなんだかpsd対応できています。`PSDImporter.cs`を見るとその理由がわかります。

```csharp:Editor/PSDImporter.cs#L25
[ScriptedImporter(23100002, new string[] {"psb"}, new[] {"psd"}, AllowCaching = true)]
public partial class PSDImporter : ScriptedImporter, ISpriteEditorDataProvider
{
    ...
}
```

https://github.com/needle-mirror/com.unity.2d.psdimporter/blob/9.0.1/Editor/PSDImporter.cs#L25

パラメータ名が書いていないので少しわかりづらいですが、第2引数が対象拡張子（`exts`）、第3引数が「オーバライド対象の拡張子（`overrideExts`）」となっています。これはUnity2021で追加されたオーバーロードのようです。

https://docs.unity3d.com/ja/2021.2/ScriptReference/AssetImporters.ScriptedImporterAttribute.html

ということで、PSDを直接対象とはせず`overrideExts`の方に入れることで、デフォルト適用はされないものの、インポータのインスペクタから切り替えられるようになるようです。

ちなみに、2019までは`AutoSelect`がこの代わりだったみたいです。

https://forum.unity.com/threads/scriptedimporter-replace-existing-unity-importer.507837/#post-6270927

# 無理やりデフォルトにする

AssetPostProcessorを使うことで、アセットがインポートされる直前に差し替えてしまうことができます。そして、フィルターも自由にかけることができるので、拡張拡張子（？？なんて呼ぶのでしょうか？）のようなものだけに対応することもできます。

```csharp
class Processor : AssetPostprocessor
{
    void OnPreprocessAsset()
    {
        if(assetPath.EndsWith(".neg.png"))
        {
            var overrideImporter = AssetDatabase.GetImporterOverride(assetPath);
            if (overrideImporter == null)
            {
                AssetDatabase.SetImporterOverride<NegaPosiPngImporter>(assetPath);
            }
        }
    }
}
```

これで、`*.neg.png`だけ`NegaPosiPngImporter`でインポートすることができます。

```csharp
[ScriptedImporter(1, new[] { "__png__" }, new [] { "png" })]
class NegaPosiPngImporter : ScriptedImporter
{
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var png = new Texture2D(2, 2);
        png.LoadImage(System.IO.File.ReadAllBytes(assetPath));
        png.Apply();
        ctx.AddObjectToAsset("main obj", png);
        ctx.SetMainObject(png);

        var nega = new Texture2D(png.width, png.height){ name = $"nega {png.name}"};
        nega.SetPixels(png.GetPixels()
            .Select(c => new Color(1 - c.r, 1 - c.g, 1 - c.b, c.a))
            .ToArray());
        nega.Apply();
        ctx.AddObjectToAsset("nega obj", nega);
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/cbf2dba0-6b98-2c0b-5096-8d07d3064985.png)

ちなみに、このインポータを変更したという情報はMETAファイルにシリアライズされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/bf83faf5-4a2c-06cf-f162-7ed0eb22d561.png)


# C#ファイル（MonoScript）は無理

残念ながら、C#ファイルではこのインポータを切り替える機能は封印されていました。

```csharp
[ScriptedImporter(1, new[] { "__cs__" }, new [] { "cs" })]
public class MonoScriptImporter2 : ScriptedImporter
{
    public override void OnImportAsset(AssetImportContext ctx)
    {
        // ↓ このメソッド（`CreateMonoScript`）も使えるのかよくわからない謎API
        // （どの道、ここまでたどり着かない）
        var cs = MonoScripts.CreateMonoScript(File.ReadAllText(assetPath), "", "", "", false);
        ctx.AddObjectToAsset("main obj", cs);
        ctx.SetMainObject(cs);

        ctx.AddObjectToAsset("txt obj", new TextAsset(File.ReadAllText(assetPath)));
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/5bb3794a-92c2-35ac-81e7-904d2eadff1c.png)

また、AssetPostProcessorによる方法についてですが、MonoScriptは`OnPreprocessAsset`で呼び出されません。代わりに`OnPostprocessAllAssets`で`SetImporterOverride`しても、無視されてしまいます。

```csharp
AssetDatabase.SetImporterOverride<NegaPosiPngImporter>(assetPath);
Debug.Log(AssetDatabase.GetImporterOverride(assetPath)); // null
```

# まとめ

従来、Unityの組み込みで対応している拡張子にはScriptedImporterを作ることができませんでした。しかし、ScriptedImporterAttributeで、拡張子を直指定するのではなく、`overrideExts`に指定することでインスペクタから選択可能になります。また、`AssetPostProcessor.OnPreprocessAsset`で`AssetDatabase.SetImporterOverride`することで、自動選択させることができ、実質的に組み込み対応の拡張子のインポータを上書きすることができました。しかし、C#ファイルのMonoScriptAssetに関しては特別に不可能でした。

組み込みのインポータが気に入らない方は、ぜひ試してみてください！

# プロジェクト

https://github.com/wallstudio/TechArticleSamples/tree/master/SetImporterOverride/Assets