【Unity】ShaderGraphを用いて動く2D向けハーフトーン(Halftone)を作ってみた

# はじめに

こんにちは、Putinuです。

本記事では、UnityのShaderGraphの機能を用いて**動く2Dのハーフトーンシェーダー**を制作していきます。
技術記事を書くことにはまだ不慣れのため、**誤字脱字・表現の誤り等**が含まれている可能性があります。
発見した際はコメントや私のTwitter（[@putinu3](https://twitter.com/putinu3)）の方にでもご連絡頂けると幸いです。

### 本記事作成の経緯

時折お洒落なUI・影の表現として登場するハーフトーンですが、「作りたい！」と興味を持ち調べてみたところ、あまりUI周りに関連した記事を見つけることが出来ませんでした。（3Dの影に関するハーフトーンの記事は沢山見られましたが...。）

であれば、自身でUI向けのハーフトーンシェーダーを制作し、一例として記事でご紹介できればと考えました。
興味がありましたら、ご一読ください。

### 注意点

- 本記事では、**UIのハーフトーンシェーダーの作り方にのみ**にフォーカスしています。
    - 3D向けのハーフトーンシェーダーについては触れません。
- UIを表示する`Canvas`について、`Screen Space - Overlay`では**動作しません**。
    - `Screen Space - Camera`のみで正しく動作します。
    - 詳細は後述します。

# 今回作るもの

本記事では以下のようなものを制作していきます。

https://twitter.com/putinu3/status/1570797925613326342?s=20&t=6_dKVRaYpa3q2lQ-g9l-mw

また、このシェーダーの全体図とプロパティは以下の通りです。
![overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/a636d697-46fb-2aae-7adb-cd8ca12fc6e1.png)
|プロパティ名|型|説明|
|-----------|-|---|
|MainTex|Texture 2D|`Sprite`のテクスチャ情報を保持する|
|Roughness|Float|マルの粗さ|
|MaxCircleSize|Float|マルのサイズ上限値|
|HideOffset|Float|ハーフトーンの消える位置|
|HideDistance|Float|ハーフトーンが消えるまでの距離|
|Rotation|Float|`UV`の回転量|
|MoveSpeed|Float|ハーフトーンの移動速度|
|MoveDirection|Vector 2|ハーフトーンの移動方向|

### 環境
- Unity 2021.3.9f1 Personal
- Universal RP 12.1.7

# 1. ハーフトーン（Halftone）とは

https://ja.wikipedia.org/wiki/%E7%B6%B2%E7%82%B9

> 網点（あみてん）またはハーフトーン（英: Halftone）とは、グレイスケールやカラーの画像を限られた色数（例えば、白い紙上の黒い点など）の小さな点のパターンで表すことで印刷可能にしたものである。

マル（水玉模様）の大きさを調整することによって、影などの**色の濃淡**を表現していたようです。

主に印刷のために用いられていたようですが、今では**デザインの一種**としても使用されています。
ゲームでも、UIの一部に使用されている例がいくつか見られました。
以下リンクより、ハーフトーンを利用した様々なゲームUIを確認することができます。

https://www.gameuidatabase.com/index.php?set=1&tag=145

# 2. 準備する

シェーダーを作るための環境を準備します。

:::note warn
UnityやShaderGraphの基本的な操作については触れません。
適宜チュートリアルページをご確認ください。
:::

## 2.1. ファイルの作成
まず、`2D (URP)`で新しいUnityプロジェクトを作成します。

プロジェクトが起動したら、`Project`ウィンドウ内で右クリックから、`Create > Shader Graph > URP > Sprite Unlit Shader Graph`を選択し、新しいファイルを作成します。

ファイルを開くと、以下のようなウィンドウが表示されます。
![0_overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/83af01b9-8cfb-3ced-b402-0cbe2f35512a.png)

## 2.2. 表示するUIの配置
`Hierarchy`ウィンドウ内で右クリックし、`UI > Image`を選択し、今回のシェーダーを適用するゲームオブジェクトを作成します。
使用する`Sprite`については、独自で用意するか、以下トグル内の素材を使用してください。
<details><summary>Sprite素材</summary>

中身（左）と外側（右）に分けています。中身（左）にのみシェーダーを適用します。
![halftone_body.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/287e386f-2e52-7d39-464b-ac7865168309.png)![halftone_frame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/b1d1703f-28ba-2c52-bf0c-3405d7e8b418.png)

</details>

作成すると、その親オブジェクトとして`Canvas`が作成されるのでそれを選択します。そして、`Inspector`ウィンドウ内の`Canvas > Render Mode`を`Screen Space - Camera`へと変更しましょう。
`Render Camera`に`Main Camera`をアタッチすることも忘れずに。
![0_Canvas.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/a3381c24-448d-bc8e-b123-9bfedf155d0a.png)

### Screen Space - Overlayにおける問題
2022/9/18現在、Unity2021.2以降において、`Canvas`の`Render Mode`を`Screen Space - Overlay`に設定している場合、`UI`にシェーダーを適用しようとすると**透過すべき部分が透過されず、真っ黒になってしまう**というバグ（？）が存在しています。

`2D Object`として利用する場合であれば問題ありませんが、`UI`として使用する場合には以上の問題が発生するため注意が必要です。

Unityのフォーラムにおいても、この問題に関するスレッドが立ち上げられていますが、現在のところ改善される様子はなく、素直に`Screen Space - Camera`を使用することが解決策として挙げられています。

https://forum.unity.com/threads/shader-graph-ui-image-shader-does-not-work.1202461/

## 2.3. Materialの作成・アタッチ

`Project`ウィンドウより、先ほど作成したシェーダーファイルを選択した状態で右クリックし、`Create > Material`からシェーダーを適用した状態の`Material`を作成します。

そしてシェーダーを適用する`Image`を`Hierarchy`ウィンドウから選択し、先ほど作成したマテリアルを`Inspector`ウィンドウ内の`Material`欄にアタッチさせましょう。
![0_Image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/9ac7867b-e093-d236-4daa-999361387009.png)
以上で準備は完了です。

# 3. 要素ごとに作っていく

はじめに、今回のハーフトーンシェーダーを作る上で**必要な構成要素**を考えます。
その上で実際にUnity上でシェーダー作成を行っていきます。

今回は以下の手順で進めることとします。
1. ハーフトーンの形を作る
1. 縦横比を維持できるようにする（綺麗なマルにする）
1. 位置に応じてマルを小さくする
1. 回転させる
1. 移動させる
1. テクスチャに合わせる

## 3.1. ハーフトーンの形を作る
ハーフトーンの模様を作るにあたって、以下のことを意識する必要があります。

- 大きさが変化する
- 等間隔にある（パターンになっている）
- 移動する

まず「大きさが変化する」ことに関しては、中心が`1`、外側が`0`となっているものを、`Step`関数を用いて**二値化する**ことで表現が出来るでしょう。
> ![Center1_Circle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/598d9067-ecd7-efc4-63e7-3934b1460c34.png)
> 中心が1、外側が0のイメージ画像

続いて「等間隔にある」ことは、**揺らぎの無い**`Voronoi`ノードを使用して`Angle Offset`を`0`にする。
「移動する」ことは、その入力に使用する`UV`を**スクロールさせる**ことで表現が出来そうです。
![voronoi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/8b77d991-8f75-8da1-f81e-410c387f2bb8.png)
以下画像のように配置し、出力先を`Alpha`に繋げます。
![1_overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/30231adc-d05a-e434-da22-618f2bb8cec0.png)
`Voronoi`ノードは各マルの中心が黒色の`0`になっています。そのため、`Step`関数を用いて二値化した後、`One Minus`ノードを使用して**値を反転**させています。

また、`Voronoi`ノードの`Call Density`については、**粗さを自由にカスタマイズできるように**`Float`型のプロパティ`Roughness`を用意し、接続しておきます。

ここまで作った状態で`Save Asset`を実行すると、以下のような状態になっています。
> Roughness : 5
![1_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/4144b73a-7e9d-2b48-ecdd-e7ce39a74b62.png)


## 3.2. 縦横比を維持できるようにする

上画像のように縦長や横長の`Sprite`を扱う場合は、それに合わせて`UV`が`0~1`の範囲内で**引き延ばされてしまいます**。
その結果、横に引き延ばされたマルが描画されてしまいます。以上のような問題を解決するために、**縦横比を維持させる必要**があります。

具体的には、現在`0~1`の範囲しか持っていない`UV`の範囲を`Sprite`の縦横比に合わせて変化させる必要があります。

ここでは`Sprite`の縦の長さを`1`として**固定**します。
そして、横の長さが縦の長さに対してどれほど長いかを調べ、その**比率**で元の`UV`と掛け合わせます。
これで`UV`の**範囲を変化**させることができます。

`Sprite`の縦横の長さをそれぞれ`w`、`h`とし、初めの`UV`の縦横の長さを`u0`、`v0`と置くと、算出される`u`、`v`は、

```math
(u, v) = (\frac{w\cdot {u_0}} h, v_0)
```

となります。
<details><summary>具体例（縦：100、横：200のとき）</summary>

例えば、`Sprite`の縦の長さが`100`、横の長さが`200`とします。
この場合であれば、上記の計算式に落とし込むと、

```math
(u, v) = (2u_0, v_0)
```

となります。つまり、**u軸方向（横方向）に2倍すること**で綺麗なマルを描画できるようになります。

</details>

ShaderGraphでは、`TexelSize`ノードを用いることで、テクスチャの縦横の長さを取得することができます。
今回はこれを用いて、縦横比を維持できるようにします。
![2_texelsize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/1653bfb9-0da1-cd59-382f-17233e3bedd1.png)
`Texture`に接続している`MainTex`プロパティは、画像左下の`+マーク`から`Texture 2D`を選択し、名前を`MainTex`として作成します。

:::note warn
プロパティの`Reference`名は`_MainTex`となっていることを確認してください。
:::

先ほどの等式を`ShaderGraph`に落とし込むと、以下の画像のようになります。
![2_overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/8c71087f-fc06-514a-d570-0a64e30a3af4.png)
縦横比を維持した状態の値が一番右側の`Vector 2`ノードより出力されています。
この出力値を`Voronoi`ノードの`UV`と接続することで、綺麗なマルへと戻すことができます。
![2_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/65549268-0b69-d542-5bda-f8ce1aca651e.png)


## 3.3. 位置に応じてマルを小さくする

今回、ハーフトーンは`Voronoi`ノードの出力値を`Step`関数で二値化することで表現するようにしました。
また、この**境界値を変化させる**ことで**マルの大きさを変化させる**ことが出来るようにしました。

よって、位置に応じてマルを小さくするには、`UV`座標に応じて**二値化する境界値を変化できるように**すれば良さそうです。
### 3.3.1. UVと直接接続する
![3_connectToStep.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/fe03701d-7436-27dd-7c41-b979c822963f.png)
`UV`ノードの出力を`Step`ノードの`Edge`に接続しました。
すると、以下画像のように左端が大きさ`0`、右端が大きさ`1`のハーフトーンが作成されました。
![3_1_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/b97120d6-da02-851f-9d9a-eec58a8e6674.png)

### 3.3.2. 上限を設ける
続いて、マルのサイズに**上限を設けられるように**します。
上限・下限を設定するためには、`Clamp`ノードを利用することが出来ます。

`UV`ノードと`Step`ノードの間に配置し、`Max`に新しく`Float`型のプロパティ`MaxCircleSize`を設定します。
`Min`についてはお好みでどうぞ。
![3_2_clamp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/1968e729-d217-bfc9-161a-6c12920b2b28.png)
これで`Step`ノードへ与える境界値に制限を与えることが出来ました。

### 3.3.3. 消える位置を調整できるようにする
次に、ハーフトーンが消えていく位置を調整できるようにします。

元々持っている`UV`座標の位置をずらすことにより、ハーフトーンの消える位置を調節します。
以下のように、境界値が`0`となる位置をずらすイメージです。
![3_3_move.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/a284803e-c133-6b5a-9168-22f319addbbb.png)
位置を変えるためには、`Add`ノードを用いて**オフセット値を加算**します。
ここで加算するオフセット値は`HideOffset`としてプロパティ化し、自由に後から変更出来るようにします。
![3_3_offset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/cd86aebd-f0cf-51f2-254c-f802b9178934.png)

### 3.3.4. 消える距離を調整できるようにする
最後に、ハーフトーンが消えるまでの距離を調整できるようにします。

以下のイメージのように、境界値を減少させる幅を**指定倍数伸ばす**ことで調整できるようにします。
![3_4_divideImg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/dd8e01a6-a316-8d83-b076-9b0fa445dc51.png)
これを表現するためには、割り算の`Divide`ノードを使用し、**割られる数**として新しくプロパティ`HideDistance`を用意します。
![3_4_divide.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/665666ce-4daf-f15c-2073-4904f5f8c2dd.png)
ここまでの配置が終わると、シェーダーをアタッチした`Material`内のプロパティを調整することで、ある程度ハーフトーンらしい調整が可能になってきます。
> MaxCircleSize : 1
> HideOffset: -0.7
> HideDistance : 0.5
![3_4_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/296aa407-3943-32f2-3285-e331f1d13d83.png)

## 3.4. 回転させる

ここからは、これまでの内容に回転の要素を入れていきます。
回転についても、これまでと同様に、`UV`座標に変化を加えることで表現することができます。

回転の表現には、`Rotation`ノードを使用し、`UV`ノードと`Divide`ノードの中間に接続します。
また、この時`Rotation`の値を`Rotation`という名前でプロパティ化し、自由に回転出来るようにします。
![4_overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/3dcc9c26-2e79-26f1-b6ea-7c83fb55a9fa.png)
以上のように配置すると、以下画像のように傾いたハーフトーンを作成することが出来ます。

> Rotation : -0.2
![4_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/ecd3fd6b-d745-c4dc-3da4-b768eab7ba36.png)


## 3.5. 移動させる

ついにハーフトーンを動かせるようにします。
ここでは、ハーフトーンを表現するために使用していた`Voronoi`ノードの`UV`を**スクロールする**ことで移動を表現します。

### 3.5.1. とりあえず動かす
移動を表現するためには、まずある地点の`UV`座標が**t秒後どんな位置にあるか**を考える必要があります。
始点を`0`として、距離を`d`、移動速度を`v`、時間を`t`として表すと、
```math
d = vt
```
と書きあらわすことができます。
これをグラフ上に落とし込んでいきます。

まず、`Move`グループ内にて経過時間を示す`Time`と、新しく作成した移動速度のプロパティ`MoveSpeed`を掛け合わせた値を用意します。
そして、それを縦横比を維持した`UV`座標の出力値と加算し、`Voronoi`ノードの`UV`と接続します。
![5_overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/79658d98-a7a3-645f-5dd4-b90dbabf9f77.png)
以上の状態で`Save Asset`から保存すると、**斜め方向へ**ハーフトーンがスクロールするようになっています。

### 3.5.2. 移動方向を与える
現在のままでは**一方向にしか移動できません**。
そのため、新しい`Vector 2`型のプロパティ`MoveDirection`を作成することで**移動方向を与えられるように**します。

ここで注意すべきは、**移動速度は既に設定されている点**です。
`MoveDirection`の値を掛ける際には、掛ける前に一度**正規化**を行い、ベクトルの大きさを`1`にする必要があります。

以上の内容を取り入れると、以下グラフのようになります。
![5_2_overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/fd3280b8-2793-51a2-a3d8-5900d6909a97.png)
`Normalize`ノードを使用し、移動方向を正規化したベクトルと、元々の出力とで掛け合わせています。
そして、そこで得られた出力値を新しく`Add`ノードと繋ぎ直します。

`One Minus`ノードについては、**移動方向の調整**という意味で配置しています。

ここで、`MoveDirection`の値を`(x, y)=(-1, 0)`とすると、**左方向へ移動**していくことが確認できます。

## 3.6. テクスチャの形状に合わせる

ここまでで大方の調整は完了しました。
ここからは、取り付けているテクスチャの**形状に合わせる**作業を行います。

まず、テクスチャの形状や色を取得するために、`Sample Texture 2D`ノードを使用し、`Texture`に`MainTex`プロパティを接続します。

テクスチャ内でアルファ値が`0`の部分に関しては映す必要が無い**透過部分**のため、これまで`Alpha`に出力していた値と**アルファチャンネルの不透明度**を掛け合わせます。
以下グラフのように、アルファチャンネルと`One Minus`ノードの出力を`Multiply`ノードを使用して掛け合わせ、`Alpha`に繋ぎ直します。

色に関しては`Sample Texture 2D`ノードの`RGBA`を`Base Color`に接続することで表現することが出来ます。
![6_overall.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/9989a596-d3bb-4382-1073-e384a90da4eb.png)
ここまで作成できれば、完成となります！
`Save Asset`から保存し、確認すると、今回の目的であった**UIの形状に沿って動くハーフトーン**を見ることが出来ます。
![6_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/986f4e76-aae6-041d-70f7-7815e97a870f.png)

# 4. まとめ

以上、`ShaderGraph`を用いた、動く2D向けハーフトーンの制作の手順紹介でした。
ここまでの手順をざっくりとまとめます。
1. `Voronoi`ノードを使用して、ハーフトーンの形を作る
1. `Texel Size`ノードをもとに、縦横比を維持できるようにする
1. `UV`座標に応じてマルを小さくする
1. `UV`を回転させる
1. `UV`スクロールで移動させる
1. `Sample Texture 2D`ノードで形状や見た目を整える

# 5. あとがき

ここまで記事を読んでくださり、ありがとうございました。

今回作成したシェーダーは、`UI`のアクセントや装飾としてのみならず、**画面遷移**などの場面でも活用できる余地がありそうですね。

また、今回のハーフトーンは縦横が揃ったものになっていますが、マルの位置が**段ごとに交互になっている**ようなハーフトーンを作ってみるのも面白いかもしれません。
これは今後の課題にしたいと思います。

筆者なりに、ある程度利用しやすいように・汎用化できるように制作しましたが、もっと様々な活用の仕方はあると思いますので、是非読者の方々もここから自由に拡張して頂ければと思います。

それでは。

# 参考文献

https://ja.wikipedia.org/wiki/%E7%B6%B2%E7%82%B9

https://www.gameuidatabase.com/index.php?set=1&tag=145

https://forum.unity.com/threads/shader-graph-ui-image-shader-does-not-work.1202461/

https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Node-Library.html