ShaderGraphのシステムを使ってShaderGeneraterを作る

# 前書き

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

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

# はじめに

UnityにはノードベースでShaderを作成することができる「ShaderGraph」という機能が搭載されています。ShaderGraphはとても複雑なシステムですが、最終的にはテキストとしてShaderLabコードを吐き出しShaderアセットとしてふるまいます。

とはいっても、ShaderLabコードをテキストファイルとして`File.WriteAllText`しているわけではありません。ソースコードは直接ファイルとして保存されることはありません。`ShaderUtils.CreateShaderAsset`というAPIを通してオンメモリにShaderAssetオブジェクトを生成します。

https://github.com/Unity-Technologies/Graphics/blob/fc8bcb7b/Packages/com.unity.shadergraph/Editor/Importers/ShaderGraphImporter.cs#L127

https://docs.unity3d.com/ScriptReference/ShaderUtil.CreateShaderAsset.html

これを活用することができそうです。

# ScriptedImporter

これは独自の拡張子に対して、インポート設定を実装することができる機能です。ShaderGraphもこれを使って実装されており、`.shadergraph`拡張子に対するインポータです。

```csharp
[ScriptedImporter(132, Extension, -902)]
public class MyShaderImporter : ScriptedImporter
{
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var bin = File.ReadAllBytes(ctx.assetPath);
        // ShaderGraphの構造体から、ShaderLabコードをGenerateする
        ctx.AddObjectToAsset($"Shader-{generatedShader.shaderName}", shader);
    }
}
```

https://docs.unity3d.com/ja/2021.3/Manual/ScriptedImporters.html

このように、インポート処理を実装します。この`OnImportAsset`には、任意の拡張子のファイルから、メモリ上にアセットを作成する処理を書きます。注意すべきは、作成したアセットがシリアライズされることはありませんし、してもいけません。これは、Unityの起動時、ファイルの更新時、ビルド時に呼ばれるので変換結果をキャッシュする必要はないのです。必要なタイミングで都度元ファイルから変換すべきということです。

どうしても保存したいデータがある場合、UserDataなどを使って、metaファイル側に保存するとよいでしょう。

https://docs.unity3d.com/ja/2020.3/ScriptReference/AssetImporter-userData.html

# シンプルな例

これの良い使い道としては、shaderlabより抽象的な独自言語などのソースコードを、shaderlabによるShaderアセットとして読み込ませるなどがあげられます。例えば、MetalShaderを書き`.msl`に対してScriptedImporterを作り、ShaderLab（HLSL）へのコンパイラを実装することも可能そうです。

ここではシンプルに、ShaderLabにカラーテクスチャを1枚セットにした`.myshader`ファイルを作りましょう。このように、#0000FFFF（blue）のテクスチャを生成する命令を1行目に追加してみました。

```hlsl:hoge.myshader
// Generate MainTex: #0000FFFF
Shader "Hoge"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "" {}
    }
    SubShader
    {
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            float4 vert(float4 vertex : POSITION) : SV_POSITION { return vertex; }
            float4 frag(float4 v : SV_POSITION) : SV_Target { return 1; }
            ENDHLSL
        }
    }
}
```

ScriptedImporter側の実装はこのようにします。

```csharp
[ScriptedImporter(1, "myshader")]
public class MyShaderImporter : ScriptedImporter
{
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var lines = File.ReadAllLines(ctx.assetPath);

        var code = string.Join("\n", lines[1..]);
        var shader = ShaderUtil.CreateShaderAsset(code);

        ctx.AddObjectToAsset("my shader", shader);
        ctx.SetMainObject(shader);

        var mainTex = new Texture2D(1, 1);
        var color = Regex.Match(lines[0], @"Generate MainTex: (?<color>#[0-9a-fA-F]{8})").Groups["color"].Value;
        mainTex.SetPixel(0, 0, ColorUtility.TryParseHtmlString(color, out var c) ? c : Color.white);
        mainTex.Apply();
        ctx.AddObjectToAsset("my texture", mainTex);

        var material = new Material(shader);
        ctx.AddObjectToAsset("my material", material);

        var hash = string.Join("\n", lines).GetHashCode();
        var meta = new TextAsset($"create at (utc): {DateTime.UtcNow}\nhash: 0x{hash:x8}");
        ctx.AddObjectToAsset("my meta text", meta);
        Debug.Log("imported");
    }
}
```

`.myshader`ファイルを、テキストファイルとして開き、1行目から色をパース、以降をShaderLabとして扱います。ShaderLabコードは`ShaderUtils.CreateShaderAsset`でオンメモリのShaderAssetへ変換してAddします。色もオンメモリのTexture2Dに変換してAddします。ついてに、Materialとメタデータも作っておきました。

この状態でUnityをRefreshすると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/f8ecc184-c410-2849-0e34-565d6ba24784.png)

出来ました！

# ComputeShaderもできる

嘗ては、GraphicsShaderだけだったような気がするのですが、2023ではComputeShaderも可能になっていました。（確か2022か2021では、VFXGraph専用のAPIしかなくてVFXGraph以外のComputeShaderは作れなかったような気がするのですが…）

```hlsl:fuga.mycompute
#pragma kernel CSMain
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID) {}
```

```csharp
[ScriptedImporter(1, "mycompute")]
public class MyComputeShaderImporter : ScriptedImporter
{
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var text = File.ReadAllText(ctx.assetPath);
        var shader = ShaderUtil.CreateComputeShaderAsset(ctx, text);
        ctx.AddObjectToAsset("my shader", shader);
        ctx.SetMainObject(shader);
        Debug.Log("imported");
    }
}
```

# まとめ

このように、ScriptedImporterとShaderUtilsを使うことで、公式のShader形式にとらわれないShaderを扱うことができるようになりました。Unity以外のエンジンやAPIとクロスにShaderを相互利用したい場合や、ShaderGraphなど以外の外部のノードツールを使いたい場合にも有用そうです。

# P.S.

ShaderGraphのScriptedImporterコードの中に、ComputeShaderをCreateしているコードもあったのですが…まさか、いつの間にかShaderGraphがComputeShaderにも対応してる？？（神）

https://github.com/Unity-Technologies/Graphics/blob/fc8bcb7b/Packages/com.unity.shadergraph/Editor/Importers/ShaderGraphImporter.cs#L163-L168