【Unity】Shaderのスクリプトをどう分割するか

# はじめに
Shaderのコードは分割したほうが使い回しできる箇所が多くなって便利になると思っていますが具体的にどう分割してるのか書かれた記事を見たことが無いので書こうと思いました。

前提条件として以下があります。
* Unity環境
* HLSLを使う(別にCgでも問題ないけど今回の例がHLSL)

# ソースコード
https://github.com/ayaha401/PnShader/tree/main/PnShader/Shader/SpriteSimpleLit

# いつもこうしてる
* ○○.shader
* ○○Core.hlsl
* ○○Macro.hlsl
* ○○SDF.hlsl
* ○○Function.hlsl
* ○○ForwardPass.hlsl

上から順番にincludeされて行きます。
Unityの標準で存在してるShaderも似たような分割になってるはず…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/e9a6496a-5baf-3e0a-cd65-02bcf33f8f63.png)
このように分割するメリットは機能ごとのShaderでよく使うメソッドたちを何回も書かなくても良くなることです。
図の**よく使うメソッド達**に該当するのは
* ○○Macro.hlsl
* ○○Function.hlsl
の2つです。
具体的にどんなことを記述していくのかは下で細かく紹介します。

# 何をどこに書くか
**○○.shader**
``Properties``と必要な数の``Pass``

**○○Core.hlsl**
``CBUFFER_START(UnityPerMaterial)``で書く内容、``struct``

**○○Macro.hlsl**
マクロ関係

**○○SDF.hlsl**
SDF関係

**○○Function.hlsl**
どこでも使いそうなメソッド

**○○ForwardPass.hlsl**
頂点シェーダーとフラグメントシェーダー


## ○○.shader
``` HLSL
Shader "Universal Render Pipeline/Pn/SpriteSimpleLit"
{
    Properties
    {
        // Main
        _MainTex("Sprite Texture", 2D) = "white" {}

        // Outline
        _UseOutline("Use Outline", int) = 0
        _OutlineColor("Outline Color", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags 
        {
            // 省略
        }
        Pass
        {
            Name "Universal2D"
            Tags
            {
                "LightMode" = "Universal2D" 
            }

            HLSLPROGRAM
            // 一部省略
            #include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteSimpleLitCore.hlsl"
            #include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteLitForwardPass.hlsl"
            ENDHLSL
        }

        Pass
        {
            Name "UniversalForward"
            Tags 
            { 
                // 省略
            }

            HLSLPROGRAM
            // 一部省略
            #include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteSimpleLitCore.hlsl"
            #include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteLitForwardPass.hlsl"
            ENDHLSL
        }
    }
}
```
``Properties``と必要な数の``Pass``を書きます。
``CBUFFER_START(UnityPerMaterial)``の中の内容は他の``Pass``で使わない変数があったとしてもすべての``Pass``で共通して書いてないとうまく働かないのでここでは書かず、``○○Core.hlsl``に書いて共通化します。

## ○○Core.hlsl
``` HLSL
#ifndef PN_SPRITE_SIMPLELIT_CORE_INCLUDED
#define PN_SPRITE_SIMPLELIT_CORE_INCLUDED

#include "Assets/AyahaShader/PnShader/Shader/Pn_Macro.hlsl"
#include "Assets/AyahaShader/PnShader/Shader/SpriteSimpleLit/Pn_SpriteFunction.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

// Texture
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

CBUFFER_START(UnityPerMaterial)
// Main
uniform half4 _MainTex_ST;

// Outline
uniform int _UseOutline;
uniform float4 _OutlineColor;
CBUFFER_END

struct Attributes
{
    // 省略
};

struct Varyings
{
    // 省略
};

#endif
```
前述のとおり、``CBUFFER_START(UnityPerMaterial)``を切り離してこのようにすれば一部のPassで変数を書き忘れてうまく動かない事故を防げます
Macroやよく使うメソッドたちはこのタイミングでincludeします。

## ○○Macro.hlsl
```HLSL
#ifndef PN_MACRO
#define PN_MACRO

#define PN_EPS .000001
#define PN_COMPARE_EPS(n) max(n, PN_EPS)

#endif
```
どこでも良く使うかもしれないマクロをここにまとめます。
## ○○SDF.hlsl
```HLSL
#ifndef PN_SDF
#define PN_SDF

float sdCircle(float2 p, float r)
{
    return length(p) - r;
}

float opUni( float d1, float d2 ) 
{
    return min(d1,d2); 
}

float opSub( float d1, float d2 )
{
    return max(-d1,d2); 
}

float opInt( float d1, float d2 ) 
{
    return max(d1,d2); 
}

#endif
```
SDF式だったり、それらを合成するためのメソッドをここに書きます。
## ○○Function.hlsl
```HLSL
// Outline
// ex) float outline = Outline(_MainTex, sampler_MainTex, i.uv, mainTex.a, width);
float Outline(Texture2D tex, SamplerState state,  float2 uv, float alpha, float2 outlineWidth)
{
    float leftShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x + outlineWidth.x, uv.y)).a;
    float rightShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x - outlineWidth.x, uv.y)).a;
    float upShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x, uv.y + outlineWidth.y)).a;
    float downShift = SAMPLE_TEXTURE2D(tex, state, float2(uv.x, uv.y - outlineWidth.y)).a;

    return saturate((leftShift - alpha) + (rightShift - alpha) + (upShift - alpha) + (downShift - alpha));
}

// https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Colorspace-Conversion-Node.html
float3 RGBtoHSV(float3 In)
{
    float4 K = float4(0.0, -0.333333333, 0.666666667, -1.0);
    float4 P = lerp(float4(In.bg, K.wz), float4(In.gb, K.xy), step(In.b, In.g));
    float4 Q = lerp(float4(P.xyw, In.r), float4(In.r, P.yzx), step(P.x, In.r));
    float D = Q.x - min(Q.w, Q.y);
    float  E = 1e-10;
    return float3(abs(Q.z + (Q.w - Q.y)/(6.0 * D + E)), D / (Q.x + E), Q.x);
}

// https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Colorspace-Conversion-Node.html
float3 HSVtoRGB(float3 In)
{
    float4 K = float4(1.0, 0.666666667, 0.333333333, 3.0);
    float3 P = abs(frac(In.xxx + K.xyz) * 6.0 - K.www);
    return In.z * lerp(K.xxx, saturate(P - K.xxx), In.y);
}

float Monochrome(float3 col)
{
    return dot(col, float3(0.299f, 0.587f, 0.114f));
}
```
色を変換するメソッドやアウトライン機能はメソッドにしたほうが便利なので分けます。
``Macro`` ``SDF`` ``Function``はよく使う機能の集合なので頂点シェーダーやフラグメントシェーダーなどよりも早くincludeします。
3Dのシェーダーを作るときは``Function``にスペキュラを作るためのメソッドや陰影を作るメソッドなどを書いておき、複数Passになった時に同じ仕組みを何回も書かないでも良くなるようにします。
## ○○ForwardPass.hlsl
```HLSL
#ifndef PN_SPRITE_SIMPLELIT_FORWARDPASS
#define PN_SPRITE_SIMPLELIT_FORWARDPASS

Varyings UnlitVertex(Attributes v)
{
    Varyings o = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(v);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    
    // 一部省略

    // Lighting
    float3 dlColor = _MainLightColor.rgb;
    float3 dlColorHSV = RGBtoHSV(dlColor);
    dlColorHSV.z = lerp(dlColorHSV.z, 1.0, _DirectionalLightPower);
    dlColorHSV.z = PN_COMPARE_EPS(dlColorHSV.z);
    dlColor = HSVtoRGB(dlColorHSV);
    o.color.rgb *= dlColor;

    return o;
}

float4 UnlitFragment(Varyings i) : SV_Target
{
    // 一部省略

    // Outline
    float4 outlineCol = (float4)0.0;
    if(_UseOutline)
    {                    
        float2 width = max(_Width, 0.0) / _WidthMult;
        width /= _MainTex_TexelSize.zw;
        float outline = Outline(_MainTex, sampler_MainTex, i.uv, mainTex.a, width);
        outlineCol.rgb = outline.xxx * _OutlineColor.rgb;
        outlineCol.a = outline.x * _OutlineColor.a * i.color.a;
    }

    // LastColor
    float alpha = (mainTex.a * i.color.a) + outlineCol.a;
    float3 lastColor = (mainTex.rgb * i.color.rgb) + outlineCol.rgb;
    lastColor = saturate(lastColor + pixelLightColor);

    return float4(lastColor, alpha);
}

#endif
```

頂点シェーダーとフラグメントシェーダーを書いてます。
そして``Macro`` ``SDF`` ``Function``で用意したマクロやメソッドを使っています。
``Pass``ごとに多少挙動が変わってもアウトラインを作ったりの基本的な仕組みを書き間違える事故を防げます。

# おわりに
メリットは何回も書いてますが
* 複数Passになった時に同じ処理を何回も書かなくてもいい
* SRP Batcherに対応させやすい

です。
命名は人それぞれ？だったりすると思います。今回**Core**となってるところが**Input**だったりするはずです。