Unity カラーパレットとノイズによる絵作り

# 作ったもの

以下のようなエフェクトを作りました。
元画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526299/16df629c-4a2a-9c04-f6c1-7663a937920e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526299/649265d8-3df5-54cd-5019-6ef07ecec5bc.png)

これだけならシンプルなカラーパレットなので、ノイズを加える事で印刷感を足しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526299/297e1dc3-7cf0-73df-3e80-a2beabc6ed3d.png)

https://github.com/Dumble009/Dithering

上記のシーンは以下のアセットをお借りしました。
https://assetstore.unity.com/packages/3d/environments/free-medieval-room-131004

# アルゴリズム

1枚目の単純なカラーパレットを適用するシェーダは以下の通りです。

```
Shader "Hidden/ColorPalette"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color1 ("Color1", Color) = (1,1,1,1)
        _Color2 ("Color2", Color) = (1,1,1,1)
        _Color3 ("Color3", Color) = (1,1,1,1)
        _Color4 ("Color4", Color) = (1,1,1,1)
        _Color5 ("Color5", Color) = (1,1,1,1)
        _Color6 ("Color6", Color) = (1,1,1,1)
        _Color7 ("Color7", Color) = (1,1,1,1)
        _Color8 ("Color8", Color) = (1,1,1,1)
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;
            float4 _Color1;
            float4 _Color2;
            float4 _Color3;
            float4 _Color4;
            float4 _Color5;
            float4 _Color6;
            float4 _Color7;
            float4 _Color8;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                float distances[8] = {
                    distance(col, _Color1),
                    distance(col, _Color2),
                    distance(col, _Color3),
                    distance(col, _Color4),
                    distance(col, _Color5),
                    distance(col, _Color6),
                    distance(col, _Color7),
                    distance(col, _Color8),
                };

                float4 colors[8] = {
                    _Color1,
                    _Color2,
                    _Color3,
                    _Color4,
                    _Color5,
                    _Color6,
                    _Color7,
                    _Color8
                };

                fixed4 resultCol = colors[0];
                float minDistance = distances[0];

                for(int i = 1; i < 8; i++){
                    resultCol = minDistance > distances[i] ? colors[i] : resultCol;
                    minDistance = minDistance > distances[i] ? distances[i] : minDistance;
                }

                return resultCol;
            }
            ENDCG
        }
    }
}

```

ここでは、8色によるパレットを使用しています。
パレットは以下をお借りしました。

https://lospec.com/palette-list/slso8

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526299/5a2c43b4-21b0-f9bb-0dc4-fc636d8cc4ec.png)

パレットに加えてノイズを付加するシェーダは以下の通りです。

```
Shader "Hidden/Dithering"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color1 ("Color1", Color) = (1,1,1,1)
        _Color2 ("Color2", Color) = (1,1,1,1)
        _Color3 ("Color3", Color) = (1,1,1,1)
        _Color4 ("Color4", Color) = (1,1,1,1)
        _Color5 ("Color5", Color) = (1,1,1,1)
        _Color6 ("Color6", Color) = (1,1,1,1)
        _Color7 ("Color7", Color) = (1,1,1,1)
        _Color8 ("Color8", Color) = (1,1,1,1)

        _DitheringThreshold ("DitheringThreshold", Float) = 0.5
        _RandomFactor ("RandomFactor", Float) = 5000
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;
            float4 _Color1;
            float4 _Color2;
            float4 _Color3;
            float4 _Color4;
            float4 _Color5;
            float4 _Color6;
            float4 _Color7;
            float4 _Color8;

            float _DitheringThreshold;
            float _RandomFactor;

            fixed4 quantize(float4 col){
                float distances[8] = {
                    distance(col, _Color1),
                    distance(col, _Color2),
                    distance(col, _Color3),
                    distance(col, _Color4),
                    distance(col, _Color5),
                    distance(col, _Color6),
                    distance(col, _Color7),
                    distance(col, _Color8),
                };

                float4 colors[8] = {
                    _Color1,
                    _Color2,
                    _Color3,
                    _Color4,
                    _Color5,
                    _Color6,
                    _Color7,
                    _Color8
                };

                fixed4 resultCol = colors[0];
                float minDistance = distances[0];

                for(int i = 1; i < 8; i++){
                    resultCol = minDistance > distances[i] ? colors[i] : resultCol;
                    minDistance = minDistance > distances[i] ? distances[i] : minDistance;
                }

                return resultCol;
            }

            float rand(float f){
                return (frac(sin(f) * _RandomFactor) - 0.5) * 2;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                
                float randomX = rand(i.uv.x);
                float randomY = rand(i.uv.y);
                float random = rand(randomX + randomY + rand(col.r) + rand(col.g) + rand(col.b));
                if(random < _DitheringThreshold){
                    return quantize(col);
                }

                return quantize(col + rand(random) / 2.0);
            }
            ENDCG
        }
    }
}
```

冒頭のスクショのパラメータは以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526299/5e7ec898-e6ac-7772-2d6a-f76b438e603d.png)

乱数の生成は以下の記事を参考にさせていただきました。

https://qiita.com/HhotateA/items/4260a460c17ffbb539fe

シード値にピクセルのUVと色を使用しているので、カメラを動かすとノイズのかかり方も変わります。

最初に `float random` を計算し、その値が閾値以上かどうかでそのピクセルにノイズが入るかどうかが決まります。
その値をそのままピクセルに加えると明るい色だけが入ってきてしまうので、再度その `random` をシードとして乱数を再計算することで、ノイズの色をランダムにしています。
2で割る事で、ノイズの影響を抑えています。

パレットを変えると暗視ゴーグル風の見た目とかもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526299/ae3d26b4-0c59-7187-4906-0d6a7867049e.png)


(以下のパレットをベースに色を足しています)

https://lospec.com/palette-list/soft-gameboy

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526299/d0c53012-26e8-c167-c8a8-87ff470b7968.png)