Unity の HLSL シェーダー：シンプルなトゥーンシェーダーを実装する：アウトライン編

# Unity の HLSL シェーダー：シンプルなトゥーンシェーダーを実装する：アウトライン編

こんにちは、@studio_meowtoon です。今回は Unity でシンプルなトゥーンシェーダーを実装する方法を紹介します。
![hlsl_in_unity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/fa31790d-8484-4a14-45a3-6a2c8bff2eb1.png)

## 実現すること

Unity で HLSL 言語を使用したシンプルなアウトライン付きトゥーンシェーダーを作成します。

### HLSL とは？

:::note info
Unity では、シェーダープログラムを記述するために HLSL というプログラミング言語を使用します。HLSL (High Level Shading Language) は元々はマイクロソフトによって開発された、Direct3D (DirectX) で使われるプログラマブルシェーダーのためのプロプライエタリなシェーディング言語です。
:::

## これまでの記事でできたこと
Unity でシンプルなトゥーンシェーダーを実装することができました。

:::note info
こちらの関連記事で実装方法がご確認いただけます。
:::

https://qiita.com/studio_meowtoon/items/fbdb4a09dd9a205a1abd

## トゥーンシェーダーとは？

:::note info
トゥーンシェーディングはアニメのセル画のような、輪郭線やはっきりした陰影をつけるシェーディングの総称であり、セルシェーディングと呼ばれることもあります。
:::

:::note alert
今回の記事では、アニメのセル画のような輪郭線を実装しています。
:::

## Unity のシェーダーを実装する

次のシェーダーは、Unity のシェーダープログラムで書かれたシンプルなアウトライン付きトゥーンシェーダーです。

```hlsl:Assets\Shaders\Germio\ColorShadeOutLine.shader
// シェーダー名
Shader "Germio/ColorShadeOutLine"
{
    // プロパティセクション
    Properties
    {
        // カスタムプロパティ "_Color" の宣言
        _Color("Color", Color) = (1, 1, 1, 1)
        
        // カスタムプロパティ "_Strength" の宣言
        _Strength("Strength", Range(0, 1)) = 0.2
        
        // カスタムプロパティ "_OutlineWidth" の宣言
        _OutlineWidth("Outline width", Range (0.0001, 0.03)) = 0.0005
        
        // カスタムプロパティ "_OutlineColor" の宣言
        _OutlineColor("Outline Color", Color) = (0, 0, 0, 1)
        
        // カスタムプロパティ "_UseVertexExpansion" の宣言
        [Toggle(USE_VERTEX_EXPANSION)] _UseVertexExpansion("Use vertex for Outline", int) = 0
    }

    // サブシェーダーセクション
    SubShader
    {
        // "Germio/ColorShade/COLOR_SHADE" パスを使用
        UsePass "Germio/ColorShade/COLOR_SHADE"
        
        // タグの設定: レンダリングキューを透明に設定
        Tags { "Queue" = "Transparent" }
        
        // ブレンドモードの設定: アルファブレンド
        Blend SrcAlpha OneMinusSrcAlpha
        
        // シェーダーのパスを定義
        Pass
        {
            // カリングをフロントに設定
            Cull Front
            
            // プログラマブルなシェーダーの開始宣言
            CGPROGRAM
            
            // 頂点シェーダーの指定
            #pragma vertex vert
            
            // フラグメントシェーダーの指定
            #pragma fragment frag
            
            // "USE_VERTEX_EXPANSION" シェーダーフィーチャーを有効化
            #pragma shader_feature USE_VERTEX_EXPANSION
            
            // UnityCG.cginc ライブラリをインクルード
            #include "UnityCG.cginc"

            // プロパティで定義されたアウトラインの幅を定義する変数
            float _OutlineWidth;
            
            // プロパティで定義されたアウトラインのカラーを定義する変数
            float4 _OutlineColor;

            // 頂点情報を格納する構造体の宣言
            struct appdata
            {
                // 頂点の座標情報
                float4 vertex : POSITION;
                
                // 頂点の法線情報
                float3 normal : NORMAL;
            };

            // 頂点シェーダーからフラグメントシェーダーにデータを渡す構造体の宣言
            struct v2f
            {
                // 頂点のスクリーン座標
                float4 pos:SV_POSITION;
            };

            // 頂点シェーダー関数
            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 頂点の座標をクリップ座標に変換
                float3 n = 0; // 法線ベクトルの初期化

                // USE_VERTEX_EXPANSION フィーチャーが有効な場合、頂点を拡張法線を使用して計算
                #ifdef USE_VERTEX_EXPANSION
                
                // 頂点の位置から方向ベクトルを計算
                float3 dir = normalize(v.vertex.xyz);
                
                // 拡張法線をワールド空間に変換
                n = normalize(mul((float3x3) UNITY_MATRIX_IT_MV, dir));
                
                // USE_VERTEX_EXPANSION フィーチャーが無効な場合、通常の法線を使用して計算
                #else
                
                // 法線ベクトルをワールド空間に変換
                n = normalize(mul((float3x3) UNITY_MATRIX_IT_MV, v.normal)); 
                
                #endif
                
                // 法線情報からビューからプロジェクション座標に変換
                float2 offset = TransformViewToProjection(n.xy);
                
                // 座標にアウトラインのオフセットを追加
                o.pos.xy += offset * _OutlineWidth;
                return o;
            }

            // プロパティで定義されたカラー情報を格納する変数
            float4 _Color;

            // フラグメントシェーダー関数
            fixed4 frag(v2f i) : SV_Target
            {
                // 最終的なカラーをアウトラインカラーで初期化
                float4 final_color = _OutlineColor;
                
                // アルファ値をカスタムプロパティのアルファ値に設定
                final_color.a = _Color.a;
                
                // 最終出力カラーを返す
                return final_color;
            }
            
            // プログラマブルなシェーダーの終了宣言
            ENDCG
        }
    }

    // フォールバック
    Fallback "Standard"
}
```

### シェーダーで描画する

こちらは今回実装したシンプルなアウトライン付きトゥーンシェーダー(マテリアル)で描画した立方体の3Dモデルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/043baf60-de85-934f-a3f0-bc3a12508278.png)

### 各セクションの説明

以下、各部分について詳しく説明します。

```hlsl
// シェーダー名
Shader "Germio/ColorShadeOutLine"
{
    // プロパティセクション
    Properties
    {
        // カスタムプロパティ "_Color" の宣言
        _Color("Color", Color) = (1, 1, 1, 1)
        
        // カスタムプロパティ "_Strength" の宣言
        _Strength("Strength", Range(0, 1)) = 0.2
        
        // カスタムプロパティ "_OutlineWidth" の宣言
        _OutlineWidth("Outline width", Range (0.0001, 0.03)) = 0.0005
        
        // カスタムプロパティ "_OutlineColor" の宣言
        _OutlineColor("Outline Color", Color) = (0, 0, 0, 1)
        
        // カスタムプロパティ "_UseVertexExpansion" の宣言
        [Toggle(USE_VERTEX_EXPANSION)] _UseVertexExpansion("Use vertex for Outline", int) = 0
    }
```

ここではシェーダーのプロパティセクションが定義されています。プロパティはシェーダーの外部から調整可能なパラメータを提供します。

|項目|内容|
|:--|:--|
|_Color("Color", Color) = (1, 1, 1, 1)|_Color というカスタムプロパティを宣言し、その初期値を白色(1, 1, 1, 1)に設定しています。このプロパティはカラーを制御するために使用されます。|
|_Strength("Strength", Range(0, 1)) = 0.2|_Strength というカスタムプロパティを宣言し、その初期値を0.2に設定しています。このプロパティはエフェクトの強度を制御します。|
|_OutlineWidth("Outline width", Range (0.0001, 0.03)) = 0.0005|_OutlineWidth というカスタムプロパティを宣言し、その初期値を0.0005に設定しています。このプロパティはアウトラインの幅を制御します。|
|_OutlineColor("Outline Color", Color) = (0, 0, 0, 1)|_OutlineColor というカスタムプロパティを宣言し、その初期値を黒色(0, 0, 0, 1)に設定しています。このプロパティはアウトラインの色を制御します。|
|[Toggle(USE_VERTEX_EXPANSION)] _UseVertexExpansion("Use vertex for Outline", int) = 0| _UseVertexExpansion というトグル型のカスタムプロパティを宣言し、その初期値を0に設定しています。このプロパティはアウトラインを頂点拡張して描画するかどうかを制御します。|

```hlsl
// サブシェーダーセクション
SubShader
{
    // "Germio/ColorShade/COLOR_SHADE" パスを使用
    UsePass "Germio/ColorShade/COLOR_SHADE"
    
    // タグの設定: レンダリングキューを透明に設定
    Tags { "Queue" = "Transparent" }
    
    // ブレンドモードの設定: アルファブレンド
    Blend SrcAlpha OneMinusSrcAlpha
```

ここではシェーダーのサブシェーダーセクションを定義されています。

|項目|内容|
|:--|:--|
|UsePass "Germio/ColorShade/COLOR_SHADE"|特定のレンダリングパスを指定し、このシェーダーがそのパスを使用することを示しています。|
|Tags { "Queue" = "Transparent" }|シェーダーのタグを設定し、透明なオブジェクトの描画に適した設定を指定しています。|
|Blend SrcAlpha OneMinusSrcAlpha|アルファブレンディングモードを設定して、透明オブジェクトの正確なブレンドを実現します。|

```hlsl
// シェーダーのパスを定義
Pass
{
    // パスの名前
    Name "COLOR_SHADE"
    
    // プログラマブルなシェーダーの開始宣言
    CGPROGRAM
    
    // 頂点シェーダーの指定
    #pragma vertex vert
    
    // フラグメントシェーダーの指定
    #pragma fragment frag
    
    // UnityCG.cginc ライブラリをインクルード
    #include "UnityCG.cginc"

    // プロパティで定義されたシェーダーの強度を定義する変数
    float _Strength;
```

ここではシェーダーのパスが定義されています。

|項目|内容|
|:--|:--|
|Cull Front|シェーダーは正面の面のみを描画し、背面をカリング（非表示）します。|
|CGPROGRAM|プログラマブルシェーダーコードを開始します。|
|#pragma vertex vert|頂点シェーダーを vert 関数として指定し、3Dオブジェクトの頂点座標や法線を操作します。|
|#pragma fragment frag|フラグメントシェーダーを frag 関数として指定し、各ピクセルの色を計算してオブジェクトの表面の色を制御します。|
|#pragma shader_feature USE_VERTEX_EXPANSION|シェーダーフィーチャー USE_VERTEX_EXPANSION を有効化します。このフィーチャーを使用して条件分岐やオプションを制御できます。|
|#include "UnityCG.cginc"|UnityCG.cginc ライブラリをインクルードし、Unityのシェーダー開発に便利な関数やマクロを利用できます。|
|_OutlineWidth および _OutlineColor|アウトラインの幅とカラーを格納する変数を宣言します。これらの変数は後続のシェーダーコードで使用され、アウトラインの描画に影響を与えます。|

```hlsl
// 頂点情報を格納する構造体の宣言
struct appdata
{
    // 頂点の座標情報
    float4 vertex : POSITION;
    
    // 頂点の法線情報
    float3 normal : NORMAL;
};

// 頂点シェーダーからフラグメントシェーダーにデータを渡す構造体の宣言
struct v2f
{
    // 頂点のスクリーン座標
    float4 pos:SV_POSITION;
};
```

ここではシェーダー内で使用するための構造体を宣言されています。

|項目|内容|
|:--|:--|
|struct appdata|頂点情報を格納するための構造体です。具体的には、頂点の座標情報（float4 vertex）と頂点の法線情報（float3 normal）を含んでいます。頂点の座標情報は頂点の3D空間座標を表し、法線情報はその頂点の法線ベクトルを表します。|
|struct v2f|頂点シェーダーからフラグメントシェーダーにデータを渡すための構造体です。通常、頂点シェーダーで計算されたデータは、フラグメントシェーダーで使用されます。この構造体は、頂点のスクリーン座標を表す float4 pos を含んでいます。SV_POSITION は、スクリーン座標を示すセマンティクスです。|

これらの構造体は、シェーダー内で頂点データの受け渡しと操作に使用されます。appdata 構造体は、入力頂点データを格納し、v2f 構造体は頂点シェーダーからフラグメントシェーダーにデータを渡すために使用されます。

```hlsl
// 頂点シェーダー関数
v2f vert(appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex); // 頂点の座標をクリップ座標に変換
    float3 n = 0; // 法線ベクトルの初期化

    // USE_VERTEX_EXPANSION フィーチャーが有効な場合、頂点を拡張法線を使用して計算
    #ifdef USE_VERTEX_EXPANSION
    
    // 頂点の位置から方向ベクトルを計算
    float3 dir = normalize(v.vertex.xyz);
    
    // 拡張法線をワールド空間に変換
    n = normalize(mul((float3x3) UNITY_MATRIX_IT_MV, dir));
    
    // USE_VERTEX_EXPANSION フィーチャーが無効な場合、通常の法線を使用して計算
    #else
    
    // 法線ベクトルをワールド空間に変換
    n = normalize(mul((float3x3) UNITY_MATRIX_IT_MV, v.normal)); 
    
    #endif
    
    // 法線情報からビューからプロジェクション座標に変換
    float2 offset = TransformViewToProjection(n.xy);
    
    // 座標にアウトラインのオフセットを追加
    o.pos.xy += offset * _OutlineWidth;
    return o;
}
```

ここでは頂点シェーダー関数 vert が宣言されており、次の操作を行っています。

|項目|内容|
|:--|:--|
|v2f o|v2f 構造体の新しいインスタンス o を作成します。この構造体は、頂点シェーダーからフラグメントシェーダーにデータを渡すためのものです。|
|o.pos = UnityObjectToClipPos(v.vertex)|頂点の座標をクリップ座標に変換し、その結果を o 構造体の pos メンバーに格納します。クリップ座標は、3Dオブジェクトの最終的な表示座標を表します。|
|float3 n = 0|法線ベクトルの初期化を行います。この法線ベクトルは後で計算されます。|
|#ifdef USE_VERTEX_EXPANSION および #else|USE_VERTEX_EXPANSION フィーチャーが有効な場合と無効な場合の2つの分岐を定義します。このフィーチャーは、アウトラインの計算方法を切り替えるために使用されます。|
|float3 dir = normalize(v.vertex.xyz)|頂点の位置から方向ベクトル dir を計算し、正規化します。|
|n = normalize(mul((float3x3) UNITY_MATRIX_IT_MV, dir))|USE_VERTEX_EXPANSION フィーチャーが有効な場合、拡張法線をワールド空間に変換し、n に格納します。|
|n = normalize(mul((float3x3) UNITY_MATRIX_IT_MV, v.normal))|USE_VERTEX_EXPANSION フィーチャーが無効な場合、通常の法線ベクトルをワールド空間に変換し、n に格納します。|
|float2 offset = TransformViewToProjection(n.xy)|法線情報からビュー座標からプロジェクション座標に変換するための offset を計算します。この offset はアウトラインの描画に使用されます。|
|o.pos.xy += offset * _OutlineWidth|アウトラインのオフセットを計算し、クリップ座標の x および y 成分に加算します。これにより、アウトラインが描画された頂点座標が得られます。|

最終的にこの頂点シェーダー関数は、変換およびアウトラインの計算を行い、結果を v2f 構造体の pos メンバーに格納して返します。このデータは後でフラグメントシェーダーで使用され、アウトラインの描画に影響を与えます。

```hlsl
// プロパティで定義されたカラー情報を格納する変数
float4 _Color;
```

```hlsl

// プロパティで定義されたカラー情報を格納する変数
float4 _Color;

// フラグメントシェーダー関数
fixed4 frag(v2f i) : SV_Target
{
    // 最終的なカラーをアウトラインカラーで初期化
    float4 final_color = _OutlineColor;
    
    // アルファ値をカスタムプロパティのアルファ値に設定
    final_color.a = _Color.a;
    
    // 最終出力カラーを返す
    return final_color;
}
```

ここではフラグメントシェーダー関数 frag が宣言されており、次の操作を行っています。

|項目|内容|
|:--|:--|
|float4 final_color = _OutlineColor|final_color という変数を初期化し、その初期値を _OutlineColor というカスタムプロパティの値に設定します。これにより、最初に final_color にアウトラインの色が代入されます。|
|final_color.a = _Color.a|final_color のアルファ値をカスタムプロパティ _Color のアルファ値に設定します。これにより、最終的なカラーのアルファ値が _Color のアルファ値に置き換えられます。|
|return final_color|最終的なカラー情報を返し、フラグメントシェーダーの処理を終了します。これにより、シェーダーは最終的な出力カラーを返し、その色はアウトラインカラーと _Color のアルファ値を組み合わせたものになります。|

このフラグメントシェーダーは、最終的な描画色を設定する役割を果たします。アウトラインのカラーは初期化され、その後、カスタムプロパティ _Color のアルファ値を適用して最終的なカラーを計算して返します。

## まとめ

Unity で HLSL 言語を使用したシンプルなアウトライン付きトゥーンシェーダーを作成することができました。

:::note alert
この記事の実装例は一つのアプローチに過ぎず、必ずしも正しい方法とは限りません。他にも多様な方法がありますので、さまざまな情報を照らし合わせて検討してみてください。
:::

どうでしたか？ Window 11 の Unity で3Dゲームを開発する環境を手軽に構築することができます、ぜひお試しください。今後も Unity の開発トピックなどを紹介していきますので、ぜひお楽しみにしてください。

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/a9be7201e24d7cae1fe9

## 参考資料

https://docs.unity3d.com/ja/2023.2/Manual/SL-VertexFragmentShaderExamples.html