アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

楽しい塗り絵の時間!(#1)

こんにちは。ちゃらいプログラマです。
技術研究で塗り絵に挑戦してみたので共有したいと思います。あ、ちなみに全然楽しくないです。

f:id:charai_ahiru:20210314234214p:plain

要件は以下の2点
1.画面タッチした箇所の色を塗りつぶしたい色で塗りつぶす (塗りつぶし機能)
 ※上の図で緑色の部分をタップしたら緑色が指定した色に変わる
2.画面ドラッグ開始した箇所の色のみを指定した色で指定した太さで塗る(塗り機能)
 ※上の図で黒色の部分は開始点の場合は、緑色の部分をドラッグしても塗られない

以下の流れで処理を行いました。
1.画面タッチした座標からピクセルカラーを取得
2.シェーダーに1.で取得した色(InColor)と塗りたい色(OutColor)を渡す
3.バッファテクスチャに更新分を書き込みテクスチャをスプライトに変換する

今回は2.のシェーダー部分をどのようにしたかを説明します。
肝の部分はフラグメントシェーダで、渡された入力カラー(InColor)と各ビクセルが一致しているかを確認し、一致していた場合は塗りたい色(OutColor)で一致していない場合は各ピクセルカラーでそれぞれ出力する処理となります。

fixed4 col = tex2D( _MainTex, i.uv );

// 入力カラーと一致しているか調べる
float b = (1 - abs(sign(_InColor.r - col.r))) * (1 - abs(sign(_InColor.g - col.g))) * (1 - abs(sign(_InColor.b - col.b)));

// b == 1 なら一致している状態になるので、OutColorが設定される
// b == 0 なら一致していないので、ビクセルカラーが設定される
fixed3 c = col.rgb * abs(1 - b) + _OutColor.rgb * b;
return fixed4( c, col.a );

上記は塗りつぶしの場合の処理ですが、塗りの場合はもう少し複雑になります。
こちらもやはりフラグメントシェーダーが肝で、タッチした座標から画像のUV値を計算しシェーダーに渡し、塗り半径に収まっていてかつ入力カラーと一致している場合のみ塗りたい色を出力します。

fixed4 col = tex2D( _MainTex, i.uv );

// 入力カラーと一致しているか調べる
float b = (1 - abs(sign(_InColor.r - col.r))) * (1 - abs(sign(_InColor.g - col.g))) * (1 - abs(sign(_InColor.b - col.b)));

----- ここまでは、塗りつぶしと一緒 ------

// タッチ座標のUV値を起点とし塗り半径(BrushRadius)に収まっていてかつ入力カラーと一致している場合のみa == 1 となる
float  a = step(distance(i.uv, _Brush.xy), _BrushRadius) * b;
// a == 1なら塗り可能な場所となるので、OutColorが設定される
// a == 0 なら塗り不可能な場所となるので、ピクセルカラーが設定される
fixed3 c = col.rgb * abs(1 - a) + _OutColor.rgb * a;
return fixed4(c, col.a);

以下がシェーダー全コードになります。
(if文を使用した点と、塗りつぶしか塗りかの判定と塗りUV値を同じパラメータ(_Brush)で管理している点はご了承ください。)

Shader "Custom/Paint"
{
    Properties
    {
        _MainTex("Texture", 2D)                         = "white" {}
        _InColor("InColor", Color)                      = (1,1,1,1)
        _OutColor("OutColor", Color)                    = (1,1,1,1)
        _Brush("Brush", Vector)                         = (0,0,0,0)
        _BrushRadius("BrushRadius", Range(0.01, 0.1))   = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "RenderType" = "Transparent"
        }

        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

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

            // メインテクスチャ
            sampler2D   _MainTex;
            // 入力カラー
            fixed4      _InColor;
            // 出力カラー
            fixed4      _OutColor;
            // ブラシパラメータ
            // x,y:クリック座標のuv値
            // z  :0→塗りつぶし/1→塗り
            fixed4      _Brush;
            // ブラシ半径
            float       _BrushRadius;

            // 頂点シェーダー
            v2f vert( appdata_t v )
            {
                v2f o;
                o.vertex = UnityObjectToClipPos( v.vertex );
                o.uv = v.uv;
                return o;
            }

            // フラグメントシェーダー
            fixed4 frag( v2f i ) : SV_Target
            {
                fixed4 col = tex2D( _MainTex, i.uv );

                // 入力カラーと一致しているか調べる
                float b = (1 - abs(sign(_InColor.r - col.r))) * (1 - abs(sign(_InColor.g - col.g))) * (1 - abs(sign(_InColor.b - col.b)));

                // 塗り
                if(_Brush.z == 1 )
                {
                    // タッチ座標のUV値を起点とし塗り半径(BrushRadius)に収まっていてかつ入力カラーと一致している場合のみa == 1 となる
                    float  a = step(distance(i.uv, _Brush.xy), _BrushRadius) * b;
                    // a == 1なら塗り可能な場所となるので、OutColorが設定される
                    // a == 0 なら塗り不可能な場所となるので、ピクセルカラーが設定される
                    fixed3 c = col.rgb * abs(1 - a) + _OutColor.rgb * a;
                    return fixed4(c, col.a);
                }
                // 塗りつぶし
                else
                {
                    // b == 1 なら一致している状態になるので、OutColorが設定される
                    // b == 0 なら一致していないので、ビクセルカラーが設定される
                    fixed3 c = col.rgb * abs(1 - b) + _OutColor.rgb * b;
                    return fixed4( c, col.a );
                }
            }

            ENDCG
        }
    }
}

次回はC#側の処理を説明したいと思います。
ではまた次回!