固定間隔の破線を描くシェーダーを作ってみた

# 固定間隔の破線を描くShader作ってみた

こんな感じのShader作ってみました。

![固定長破線.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/79aec85c-3893-47aa-48f1-9ae9d12e8cf7.gif)

破線を用いた矩形の描画についてはまた別の記事にまとめるとして、今回は線の描画のShaderについて記載します。

# 直線の描画はUILineRendererが使える

直線の描画にはUILineRendererを用いて線を描画します。
UILineRendererにMaterialを設定することで線を描画することができます。
今回はUILineRendererの使い方とShaderの細かい書き方については割愛します。

UILineRendererにSpriteを設定する箇所がありますが、こちらはUISprite以外を設定しておいてください。
理由はいつか書けたらいいな、と思ってます。
書いたらこちらの記事にリンクを追加します。

## Step1：実線を描画してみた

破線を描画する前にまずは実線を描画してみます。

![実線 1.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/f2c09944-820e-17f3-4c79-053354257575.gif)

破線ではなく、実線で良ければShaderは下記でOK。
_Colorはコード上から指定しています。

```C#
fixed4 frag( v2f i ) : SV_TARGET
{
    return _Color;
}
```

## Step2：破線を描画してみた

こんな感じの破線。

![サイズがかかわる破線.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/fb29e8f4-9ee2-5ae4-e26e-6cebececeeff.gif)

step関数を利用して描画するかどうかの判定をしています。
step関数は**第一引数<=第二引数の時に1.0fを返す関数**なので、sin関数で-1.0～1.0の値を計算して破線を描画しています。
_Rateはコード上から指定しています。
i.uv.xは0.0～1.0なので、そのままだとsin(0.0)～sin(1.0)はすべて0以上で実線にしかならないので、_Rateで調整できるようにしています。


```C#
fixed4 frag(v2f i) : SV_TARGET
{
    return step(0, sin(_Rate * i.uv.x)) * _Color;
}
```
Gif画像のように拡縮しないとか、拡縮しても破線の描画する点(線？)の数をそのままにしたいならこの実装で良いかと思います。
ただ、これだと破線の間隔などが変わってしまうので、要求仕様によってはNGになってしまうかと思います。

## Step3：破線の長さや間隔を固定してみた

破線の長さを固定するには描画する線の長さをshaderに与える必要があります。
Shaderでは_MainTex_TexelSizeを設定することで縦横の解像度を取得することができますが、UILineRendererでのMainTexはSpriteで指定したものになります。

そのため、下記のように書き換えてみました

```C#
fixed4 frag(v2f i) : SV_TARGET
{
    return step(0, sin( _Length * _Ratio * i.uv.x)) * _Color;
}
```

**_Length：描画する線全体の長さ**
**_Ratio：破線の間隔の調整値**

_Lengthは今回だと矩形の各一辺を指定しています。
_Ratioの値はコード上から設定してますが、こんな感じになります。

* _Ratio=0.3
![sinの固定長(0.3).GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/342243a3-837b-7cae-1a0c-dcb99281c41e.gif)

* _Ratio=0.01
![sinの固定長(0.01).GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/900fb1c9-1294-1bdf-3eef-0ededc20feea.gif)

## Step4：破線の長さと間隔を別々に指定できるようにしてみた

Step3は破線の描画部分と描画しない部分の間隔が一緒になってしまうので間隔を設定できるようにしてみました。

![描画するのとしないのの間隔変更.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/9125526b-dea5-e140-5d17-e625259ff45e.gif)

```C#
fixed4 frag( v2f i ) : SV_TARGET
{
    // 描画しない部分がないので実線になる
    if( _Interval <= 0 )
    {
        return _Color;
    }

    // 描画する部分がないので描画なしの値でOK
    if( _Draw <= 0 )
    {
        discard;
    }

    // 描画あり＋描画なしのセットの長さ
    float drawSetLength = _Draw + _Interval;

    // 0 割り発生チェック
    if( drawSetLength <= 0.0f )
    {
        discard;
    }
                
    // 描画する位置を取得
    float drawPosition = _Length * i.uv.x;

    // 色の計算
    // fmod：余りを計算することで描画あり＋描画なしのセットのどのあたりの位置かを取得
    // step：第一引数<=第二引数の時に1.0fを返す関数
    // 　　　描画あり＋描画なし のセットで 描画あり の位置の時に1.0fを返すようにした
    return step( fmod( drawPosition, drawSetLength ), _Draw ) * _Color;

    //return outColor;
}
```

各設定値はこんな感じです。
**_Draw：描画する長さ**
**_Interval：描画しない長さ**
**_Length：線全体の長さ**

_Lengthは描画する線の長さを指定しています。(今回だと矩形の各一辺)
先程も記載したようにShaderでは_MainTex_TexelSizeを設定することで縦横の解像度を取得することができますが、UILineRendererでのMainTexはSpriteで指定したものになります。
Spriteで指定したサイズだと拡縮した時に線の長さが固定にならないため、この記述になっています。

このコードでは_Lengthを利用して拡縮後の実際の描画位置を下記のように計算しています。
```C#
// 描画する位置を取得
float drawPosition = _Length * i.uv.x;
```

下記の計算で「描画する部分＋描画しない部分」の繰り返しのうち、drawPositionが「描画する部分」と「描画しない部分」のどちらに属するか計算しています。
```C#
fmod( drawPosition, drawSetLength )
```

下記の計算で「描画する部分」に属する場合は1.0、「描画しない部分」に属する場合は0.0を返すことで描画の有無を判定しています。
```C#
step( fmod( drawPosition, drawSetLength ), _Draw )
```

### Step4で_Lengthを指定しないとどうなるのか？
Spriteのサイズを使用するため、線(点？)の描画される数は変わりませんが、伸長するだけなので間隔が拡縮によって変わってしまいます(Step2と同じ状態)

* Sprite小さめ(100pixel)
![Sprite小さめ固定長.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/538e04da-9ef8-5570-72fa-c632935dbff9.gif)
* Sprite大きめ(1000pixel)
![Sprite大きめ固定長.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/a2488a05-6405-65ea-1c37-f5235dc7fa66.gif)

# さいごに
今回の実装にはUILineRendererを使用しました。

固定の長さの破線を描くためには外部から線を描画する長さを渡す必要があります。

今回、例として示している矩形の拡縮がなければ、パラメータを調整すれば理想の破線を描けるかと思いますが、拡縮を変更するUIの場合は今回示したShaderが有効かと思います。

次回の記事ではUILineRendererを用いて破線の矩形を描く方法について記載しようと思います。