【React Three Fiber】ShaderでMeshに動きをつける

<!-- ================================================= -->
# 概要
<!-- ================================================= -->
蠕動は、Shaderを学習する過程でうまれたアプリケーションです。

https://nemutas.github.io/r3f-zendo/

![output(video-cutter-js.com) (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/c362dce4-ceb7-c9bc-1600-323e9e581985.gif)

:::note info
この記事では、蠕動に使用しているShaderを例に、実装の考え方をまとめました。
:::

>蠕動（ぜんどう、英語: peristalsis）とは、筋肉が伝播性の収縮波を生み出す運動である。
>https://ja.wikipedia.org/wiki/%E8%A0%95%E5%8B%95

<!-- ================================================= -->
# Meshに動きをつける
<!-- ================================================= -->
蠕動は、言ってしまえば、球体をサイン波で動かしてるだけの単純なShaderで成り立っています。
ここでは、動きをつけているShaderの実装についてまとめています。

MeshのMaterialに対してShaderを割り当てる方法は、以下の記事で詳細にまとめています。
[【React Three Fiber】Shaderを使用してMaterialを変更する](https://qiita.com/nemutas/items/434806b73455634a5d27)

### Shaderの設計手順
```.tsx:src/components/viewer/WaveShpere.tsx
const vertexShader = `
uniform float u_time;
uniform float u_amplitude;
uniform float u_frequency;

varying vec3 v_position;
varying vec3 v_normal;

void main() {
  // wave
  //----------------------------------
  // f1 = sin(t + x * f) -> -1 ~ 1
  // f2 = (f1 + 1) / 2   -> 0 ~ 1
  // f3 = a * f2         -> 0 ~ a
  // f4 = f3 + (1 - a)   -> (1 - a) ~ 1
  //----------------------------------
  float ratio = u_amplitude * (sin(u_time + position.y * u_frequency) + 1.0) / 2.0 + (1.0 - u_amplitude);
  vec4 pos = vec4(vec3(position.x * ratio, position.y, position.z * ratio), 1.0);
    
  // glow
  pos = modelViewMatrix * pos;
  v_position = pos.xyz;
  v_normal = normalMatrix * normal;

  gl_Position = projectionMatrix * pos;
}
`

const fragmentShader = `
uniform vec3 u_color;
uniform float u_start;
uniform float u_end;
uniform float u_alpha;

varying vec3 v_position;
varying vec3 v_normal;

void main() {
  // glow
  vec3 normal = normalize(v_normal);
  vec3 eye = normalize(-v_position);
  float rim = smoothstep(u_start, u_end, 1.0 - dot(normal, eye));
  float ratio = clamp(rim, 0.0, 1.0);
  
  // gl_FragColor = vec4(ratio * u_alpha * u_color, 1.0);
  gl_FragColor = vec4(u_color * ratio, u_alpha + ratio);
}
`
```
注目したいのは、`vertexShader`の**wave**の箇所です。

最終的に、算出した`ratio`を、Sphereの各頂点のxz座標に掛けて動きをつけています。
周期性のある関数を各頂点の座標に掛けると、**掛けた座標に関して法線ベクトル方向に伸縮**します。

:::note info
Shaderで関数を作成する場合、<strong>イメージを持ちながら順を追って関数を作っていく</strong>ことが重要だと思います。
:::

#### Step 1：時間経過で振動するsin関数を作成する
まずは、単純にsin関数だけを使用して考えます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/86dc876d-e175-ae0d-20d0-4b61014ce5be.png" width="500" />

```.tsx
float ratio = sin(u_time);
```
`u_time`は、経過時間を格納している変数です。
これはどのような関数でしょうか。それを知る便利なツールがあります。

https://graphtoy.com/

**Graphtoy**のすごいところは、**GLSLで使用できる関数**が揃っていて、さらに**時間変数**を使用できるところです。
上記のコードは、以下のように表せます。
![スクリーンショット 2021-12-19 153805.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/b295dd7a-c2b6-1e0c-5264-0b8beeb733b5.png)
時間経過で、yの値が`-1 ~ 1`まで反復していることがわかります。

ただし、この`raito`を掛けても、Sphereのすべての頂点のxz座標が均一に伸縮するだけになります。

#### Step 2：Sphereのy座標によって波形を変える
y座標によって波形を出すためには、y座標をsin関数の位相として扱います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/6831a578-9f57-00df-c87f-e004449cfcf0.png" width="500" />

```.tsx
float ratio = sin(u_time + position.y * u_frequency);
```
`u_frequency`は、振動数です。
これを、Graphtoyで確認してみます。
![スクリーンショット 2021-12-19 155052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/55000c48-188d-5b5d-0369-44c4eaa20ae6.png)
y座標（position.y）の代わりに、xを使用しています。xの値で振動しているのがわかります。

ただし、振幅（`raito`の取る範囲）が`-1 ~ 1`になっているので、xz座標がy軸対称に反転する（法線ベクトルの向きが逆になる）周期が出てきてしまいます。

#### Step 3：振幅を0~1にする
`raito`がとる範囲を`0 ~ 1`にして、座標をスケーリングしたときにy軸に対して反転しないようにします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/b18ec200-27bd-7465-b137-97903baf51a9.png" width="500" />

```.tsx
float ratio = (sin(u_time + position.y * u_frequency) + 1.0) / 2.0;
```
Graphtoyで確認してみましょう。
![スクリーンショット 2021-12-19 160915.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/b2ba0c35-9daf-7a98-4d50-017b12c087e6.png)
yの値が、`0 ~ 1`になっていることがわかります。

`raito`（Sphereのxz座標にかかるスケール・振幅）は、`0 ~ 1`に固定されているので、この振幅を変えられるようにします。

#### Step 4：振幅を変える
振幅を変えて、Sphereの動きにバリエーションをつけます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/5a8228a0-93ee-643a-76f2-9fe2f4e38f3c.png" width="500" />

```.tsx
float ratio = u_amplitude * (sin(u_time + position.y * u_frequency) + 1.0) / 2.0;
```
Graphtoyで確認してみましょう。
![スクリーンショット 2021-12-19 162229.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/87887681-264e-2e3a-49bb-7a1ae19aab01.png)
グラフを見ると、振幅は`0 ~ 0.2`となっています。ここで、蠕動の場合、振幅がとる範囲は`0 ~ 1`としています。

しかし、このまま`raito`を使用してしまうと、Sphereのxz座標は`0 ~ u_amplitude`の範囲で振動してしまいます。

#### Step 5：振動の基準を「1」にする
振動の基準を「1」に変えます。`ratio = 1`は、つまりSphereの元の座標です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/dcf68a1b-10a9-259b-0992-227283592dfa.png" width="500" />

```.tsx
float ratio = u_amplitude * (sin(u_time + position.y * u_frequency) + 1.0) / 2.0 + (1.0 - u_amplitude);
```
Graphtoyで確認してみましょう。
![スクリーンショット 2021-12-19 163109.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/9417f6b8-13a5-f0ec-ec8a-660b4b0f1424.png)
これで、Sphereの元のxz座標を基準に、振幅が大きくなるほどxz座標が0に近付く`ratio`が得られました。

<!-- ================================================= -->
# 一様な見た目をつける
<!-- ================================================= -->
蠕動の見た目もShaderで作成しています。
こちらは、以下を引用させていただいたコードになっています。

https://shaderfrog.com/app/view/5

このShaderの特徴は、**どんなアングルからみても一様な効果の見た目になる**ところです。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/0f08bb37-9e9d-84e8-fe05-93e868de2989.png)
ここで言う**一様な効果**とは、uv座標を使用しないことで端部に切れ目が発生しない、引き延ばされたりしないことを意味しています。
このようなShaderは、**カメラから見た位置座標**を利用することで描画することができます。

uv座標を使用した描画の例として、以下のような:fire:のすごいShaderがあります。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/fec9a0dd-f4ce-3b32-d522-354c83f86573.png)
Planeで見る分にはいいですが、これをSphereに割り当てると、uv座標の端部で連続性がなくなります。
また、より複雑な形状のMeshに使用すると、uv座標の関係で描画自体が引き延ばされたりします。

:::note info
イメージとしては、
uv座標を使用したShaderは<strong>描画</strong>、Meshの位置座標を使用したShaderは<strong>エフェクト</strong>という感じです。
:::

### Shaderの設計手順
```.tsx
const vertexShader = `
uniform float u_time;
uniform float u_amplitude;
uniform float u_frequency;

varying vec3 v_position;
varying vec3 v_normal;

void main() {
  // wave
  //----------------------------------
  // f1 = sin(t + x * f) -> -1 ~ 1
  // f2 = (f1 + 1) / 2   -> 0 ~ 1
  // f3 = a * f2         -> 0 ~ a
  // f4 = f3 + (1 - a)   -> (1 - a) ~ 1
  //----------------------------------
  float ratio = u_amplitude * (sin(u_time + position.y * u_frequency) + 1.0) / 2.0 + (1.0 - u_amplitude);
  vec4 pos = vec4(vec3(position.x * ratio, position.y, position.z * ratio), 1.0);
    
  // glow
  pos = modelViewMatrix * pos;
  v_position = pos.xyz;
  v_normal = normalMatrix * normal;

  gl_Position = projectionMatrix * pos;
}
`

const fragmentShader = `
uniform vec3 u_color;
uniform float u_start;
uniform float u_end;
uniform float u_alpha;

varying vec3 v_position;
varying vec3 v_normal;

void main() {
  // glow
  vec3 normal = normalize(v_normal);
  vec3 eye = normalize(-v_position);
  float rim = smoothstep(u_start, u_end, 1.0 - dot(normal, eye));
  float ratio = clamp(rim, 0.0, 1.0);
  
  gl_FragColor = vec4(u_color * ratio, u_alpha + ratio);
}
`
```

#### カメラからの位置座標を取得する
どんなアングルからでも一様な見た目にするには、カメラからの位置座標を取得する必要があります。

```.tsx
pos = modelViewMatrix * pos;
v_position = pos.xyz;
```
【Meshに動きをつける】で動きをつけたあとの座標に、[モデル変換とビュー変換](https://wgld.org/d/webgl/w003.html)を行います。
この変換は、座標に`modelViewMatrix`を掛けることで行えます。

また、法線ベクトルは`normalMatrix`を掛けたものを取得します。

```.tsx
v_normal = normalMatrix * normal;
```
`normalMatrix`は、元の法線ベクトルに座標変換の結果を適用させるために使用します。詳しくは以下を参照してください。
[法線を正しい向きにするときなぜ逆転置行列なのか](https://qiita.com/ktanoooo/items/7da443e7bc38f7ff6734)

`position，normal，modelViewMatrix，normalMatrix`はいずれもThree.jsの**Vertex Shaderに組み込まれている変数**なので、Fragment Shaderには`v_position，v_normal`変数として渡す必要があります。
[Built-in uniforms and attributes](https://threejs.org/docs/#api/en/renderers/webgl/WebGLProgram)

最終的に、Shpereのある座標（<font color="#f00">●</font>）に着目したときの`v_position`は<font color="#22A7F2">青の矢印</font>、`v_normal`は<font color="#398400">緑の矢印</font>のようなイメージになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/05d0467d-02a3-085c-2558-b16d6d4646ea.png" width="500" />

#### 色を決定する
Fragment Shaderに渡された`v_position`、`v_normal`を使用して色を決定していきます。

```.tsx
void main() {
  // glow
  vec3 normal = normalize(v_normal);
  vec3 eye = normalize(-v_position);
  float rim = smoothstep(u_start, u_end, 1.0 - dot(normal, eye));
  float ratio = clamp(rim, 0.0, 1.0);
  
  gl_FragColor = vec4(u_color * ratio, u_alpha + ratio);
}
```
`normal`は、`v_normal`を長さ1に基準化した単位ベクトルです。
`eye`は、`v_position`を逆方向にして、長さ1に基準化した単位ベクトルです。

y軸から見下ろすと、各ベクトルは以下のようになっています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/2394137c-095a-0d13-1b49-50b307bf0ac6.png" width="400" />

`rim`では、GLSLの組み込み関数`smoothstep`を使用して、normalとeyeの内積で**色に掛ける係数**を決めています。

```.tsx
float rim = smoothstep(u_start, u_end, 1.0 - dot(normal, eye));
```
Graphtoyで確認してみましょう。`u_start`と`u_end`は変数にしてますが、蠕動では0と1を使っています。
![スクリーンショット 2021-12-19 213338.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/436f5344-30a6-89c3-5485-8b66b041b1d0.png)
yの値は`0 ~ 1`で変動し、xの0 ~ 1の範囲（u_start ~ u_end）でスムーズに補間されていることがわかります。

xの値、つまり`normal`と`eye`の内積は、ベクトル同士が直行する場合が「0」、同一方向を向いていて重なる場合は「1」になります。
俯瞰図をみると、Sphereの真ん中ほど内積は「1」に近付き、端（視界に入る範囲の端）に行くほど内積は「0」に近付きます。
最終的には、Sphereの真ん中ほど黒く、端ほど色を見せたいので真偽値を逆にしています。

```.ts
1.0 - dot(normal, eye)
```

#### 一様な見た目をとるShaderのサンプル
他の実装例として、以下の素晴らしいサンプルががあります。

* [Heat Shader](https://shaderfrog.com/app/view/2)
* [Basic "Irridecent" Shader](https://shaderfrog.com/app/view/2665)
* [Basic Toon Shader](https://shaderfrog.com/app/view/3825)
* [Dot mesh](https://shaderfrog.com/app/view/5581)

<!-- ================================================= -->
# 影を描画する
<!-- ================================================= -->
蠕動では影もShaderを使用して描画しています。
蠕動する球体の動きに合わせて、影の濃さをコントロールしています。

### Shaderの設計手順
```.tsx:src/components/viewer/FakeShadow.tsx
const fragmentShader = `
varying vec2 v_uv;
uniform vec3 u_color;
uniform float u_radius;
uniform float u_smooth;

uniform float u_time;
uniform float u_amplitude;
uniform float u_frequency;

void main() {
	float dist = distance(vec2(0.5), v_uv);
	float circle = 1.0 - smoothstep(u_radius - u_smooth, u_radius + u_smooth, dist);
	//----------------------------------------------------------------------------------------------------
	// The top(1.0) and bottom(-1.0) of the sphere have a radius around the y-axis that is close to zero,
	// so the shadow is affected at -0.8 from the center.

	float ratio = u_amplitude * (sin(u_time - 0.8 * u_frequency) + 1.0) / 2.0 + (1.0 - u_amplitude);
	//----------------------------------------------------------------------------------------------------
	
	gl_FragColor = vec4(u_color * circle * ratio, 1.0 * circle * ratio);
}
`
```
今までの説明で理解できると思うので、要点だけまとめます。

* PlaneにShaderを使用して影を描画しているだけなので、uv座標を使用しています。
* uv座標は（0, 0）~（1, 1）をとり、中心は（0.5, 0.5）です。そのpixelに影を描画するかは中心からの距離で決めています。
* 影の濃さは、【Meshに動きをつける】で作成した関数とおなじものを使用します。ただし、影は平面なので`position.y`としていたところを`-0.8`にしています。<br>
Vertex Shaderでは、positionは（-1, -1）~（1, 1）の範囲で値を取るので、中心からy軸方向に0.8だけ下がった位置の振幅に併せて、影の濃さを決めています。<br/>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/2e5b9448-9376-54be-f596-dc4c79449540.png" width="500" />

ちなみに、[Drei（@react-three/drei）](https://docs.pmnd.rs/drei/introduction)にもほぼ同じことができるコンポーネントが用意されています。
こちらは、ShaderではなくCanvas Textureを使用して影を描画しているようです。

https://docs.pmnd.rs/drei/misc/shadow

<!-- ================================================= -->
# リポジトリ
<!-- ================================================= -->

https://github.com/nemutas/r3f-zendo

<!-- ================================================= -->
# まとめ
<!-- ================================================= -->
Shaderを作成するときは、小さなところから実験しながら実装していくのがいいと思います。

また、複雑な数式がバーンと書かれているとどうしても、自分で作ったもの以外は読む気が失せるので、その設計趣旨も何らかの形で残しておくのがいいと思いました。