【Unity】ParticleSystemで利用するShaderを作る

本記事は [QualiArts Advent Calender 2019](https://qiita.com/advent-calendar/2019/qualiarts) 22日目の記事です。

## はじめに
UnityでParticleSystemを利用する場合にShaderに色々な機能を付け加えてあげれば、表現の幅が広がります。
本記事では色々な機能の実装例を紹介し、ParticleSystemのShaderを作るときのヒントになればと思います。

## Shaderの雛形
今回ParticleSystemように用意をしたShaderの雛形です。

```
Shader "Sample/Default_Shader"
{
    Properties
    {
        //Main
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }

        Pass
        {
            Blend ONE ONE
            Cull BACK
            Lighting Off 
            ZWrite off
          
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                half4 color : COLOR;
                half4 normal : NORMAL;
                float4 texcoords : TEXCOORD0;
            };

            struct v2f
            {
                float4 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                half4 color : COLOR;
            };

            //Main
            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.color = v.color;
                o.uv.xy = TRANSFORM_TEX(v.texcoords.xy,_MainTex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                half4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}
```
Mainテクスチャの色をそのまま加算で描画をしています。
この雛形を色々と触っていきます。
## Blendを調整する
パーティクルを描画するときに自身の色と既存の背景の色をどのように合成するかと設定します。

```
Blend A B
```

AはSrcFactor、BはDstFactorと呼ばれます。

 - SrcFactor 対象のオブジェクトのColorを乗算します
 - DstFactor すでに描画されているオブジェクトのColorを乗算します

雛形のShaderではSrcFactor、DstFactorともにONEが設定されていました。
ONEは色に1を乗算しているので、設定されたそのままの色が描画で計算されます。

### パーティクルの主な合成方法
Unityの標準Shaderにもあるのですが、次の４つが合成方法としてよく使われています。

```
// 加算
Blend One One
// 透過加算
Blend SrcAlpha One
// アルファブレンド
Blend SrcAlpha OneMinusSrcAlpha
// 乗算
Blend Zero SrcColor
```

## ParticleSystemのCustomDataを使う
UnityのParticleSystemのCustomDataモジュールを使えば、Shaderの頂点情報に何かしらの値を流し込むことができます。
これを使うことでより複雑な表現ができます。

### Shaderの設定する
バーテックスShaderに専用の受け口を準備します。
今回はfloat型のcustomDataをストリームに定義しました。

```
struct appdata
{
    float4 vertex : POSITION;
    half4 color : COLOR;
    half4 normal : NORMAL;
    float4 texcoords : TEXCOORD0;
    float customData : TEXCOORD1;
};
```

### CustomDataを設定する
ParticleSystemのRenderモジュールにあるCustomVertexStreamsを設定します。
追加したストリームはUV2とCustom1.xyzwです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/7bfb4c17-783d-aff7-acaa-48f939530f94.png)

次にParticleSystemのCustomDataモジュールを設定します。
今回はVectorのxを使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/9847b14e-99c5-400f-e27b-b470156cf8fc.png)

## UVスクロール
今回の記事で一番使っていただきたいのが「UVスクロール」です。
パーティクルに動きがあるだけでよりリッチな表現ができます。

CustomDataで設定した値をOffsetとして、Vertexシェーダーで行っているテクスチャのUVに足します。
Vertexシェーダーでの実装で、このようなコードになります。

### 実装
Shaderのストリームを調整します。

```
float2 customData1 : TEXCOORD1;
```

バーテックスShaderでCustomDataを取り出してUV座標に足し合わせています。

```
v2f o;
half2 mainCustomCoord = half2(v.customData1.x,v.customData1.y);
o.uv.xy = TRANSFORM_TEX(v.texcoords.xy,_MainTex) + mainCustomCoord;
```

### 実装
ParticleSystemのCustomDataモジュールを調整します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/04ee86be-1481-00b9-0cd5-09a7836d4d97.png)

## ColorにHDRを設定する
発光を表現する場合など、HDRを使いたい場合Shaderで次のプロパティを作成します。

```
[HDR] _Color ("HDR COLOR",color) = (1,1,1,1)
```

Shaderのプロパティの前に[HDR]を入れるだけでIntensityがColor設定時に利用ができます。
<img width="300" alt="hdr" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/c6ffc7c5-7a64-1534-e065-05e2299e38b4.png">

Shaderではこのように使っています（単純に乗算している）

```
fixed4 frag (v2f i) : SV_Target
{
    half4 col = tex2D(_MainTex, i.uv);
    col *= _Color;
    return col;
}
```

## 歪み
エフェクトのテクスチャを歪ませることで特別な表現を実現することができます。
１枚「歪み」専用のテクスチャを用意して、こちらの色情報を元にパーティクルを歪ませます。

### 実装
バーテックスShaderでは「歪み」で使うテクスチャのUV情報を取得します。
現状使用していないuvのzとwに情報を入れます

```
o.uv.zw = TRANSFORM_TEX(v.texcoords.xy,_DistTex);
```

フラグメントShaderでの処理は次のようになります。
ColorのRの情報を使ってUVを移動しています。

```
half dist = tex2D(_DistTex, i.uv.zw).r;
half4 col = tex2D(_MainTex, i.uv.xy + dist);
```

#### 歪みの例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/824b3867-566c-0aea-6a61-640dd9fd5dd2.png)

## マスク
マスクはパーティクルを専用の画像でを使うと画像の特定の部分を隠すことができます。
「歪み」と同じ専用のテクスチャを用意します。

今回も「歪み」と同じようにUvの残り部分を利用しています。

```
o.uv.zw = TRANSFORM_TEX(v.texcoords.xy,_DistTex);
```

マスクなので算出したRの色情報をアルファに乗算しています。
コードは次のようになります。

```
half mask  = tex2D(_DistTex, i.uv.zw).r;
half4 col = tex2D(_MainTex, i.uv.xy);
col.a *= mask;
```
#### マスクの例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/e15c70e1-07fb-13ae-456a-4538741ee10c.png)

## リム
リム（Rim)とは「ふち」という意味があります。
今回のリムを使ってParticleの「ふち」のアルファ値を調整します。

### 実装
バーテックShaderではカメラと退場のオブジェクトの法線の内積を計算します。

```
v2f vert (appdata v)
{
    // ローカル => world座標へ変換
    float4 worldPos = mul(unity_ObjectToWorld,v.vertex);

    // 法線
    half3 worldNormal = UnityObjectToWorldNormal(v.normal);

    // カメラからのベクトル
    // _WorldSpaceCameraPos : カメラのworldPos
    half3 worldViewDir = normalize(_WorldSpaceCameraPos - worldPos);

    // dot(x,y) = x,yの内積
    // abs => 絶対値
    o.rimLightValue.x = abs(dot(worldViewDir, worldNormal));
}
```

フラグメントShader側の処理
今回はfloat型プロパティである_Powerを使って影響範囲を調整しています。

```
fixed4 frag (v2f i) : SV_Target
{
    half4 col = tex2D(_MainTex, i.uv);
    half rimValue = 1.0f;
    rimValue *= pow(i.customData.x, _Power);
    col.a *= rimValue;
    return col;
}
```

アルファで調整する場合はBlendを変更しないと反映されない場合がありますので、ご注意ください。

```
// アルファブレンドにすれば上手く動くはず
Blend SrcAlpha OneMinusSrcAlpha
```

#### リムの例
<img width="300" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/dac3e0ea-4421-4d08-8bc1-9291e3fbf27d.png">

## 輝度による色設定
エフェクトで利用するテクスチャを節約するために、白黒で画像を作成し、Shaderで色を設定します。

### 実装方法
プロパティを用意

```
_Color1("color1",color) = (1,1,1,1)
_Color2("color2",color) = (1,1,1,1)
```

Luminance()を使ってカラーから輝度を取得します。
それを元にピクセルごとの色を判定します。

```
half4 col = tex2D(_MainTex, i.uv.xy);
fixed luminance = Luminance(col.rgb);
col *= lerp(_Color1,_Color2,luminance);
```

#### 例 Color1 : 緑色と Color2 : 青色 設定
<img width="400" alt="色設定" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/8e1eced0-724c-eb11-028d-8d8f36f7cf93.png">

## MaterialのInspectorを拡張する
詳しくは触れないですが、Shaderを作成したときにMaterialのInspectoreを拡張して上げれば他の人が使う時わかりやすいです。
このあたりを参考にしていただければ。
[カスタムシェーダー GUI](https://docs.unity3d.com/ja/current/Manual/SL-CustomShaderGUI.html)
### 実装例
<img width="400" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61216/fd6c15f1-9256-ae53-af25-ae5e207fe065.png">

## 最後に
今回紹介した機能を全部使う場合は少ないと思いますので、必要な表現を実現するためにShaderを実装をしたほうが良いと思います。
まずは何をしたいかを明確にし、その中で今回紹介した機能が何かの役に立てればと思います。