Svelte で 3D 表示を行うライブラリ Threlte #6: カスタムシェーダーを適用する

# シリーズ

* [Svelte で 3D 表示を行うライブラリ Threlte #1: パッケージの作成とライブラリ概要](https://qiita.com/takavfx/items/0d4c6b831628d628b468)
* [Svelte で 3D 表示を行うライブラリ Threlte #2: エクストラパッケージを使用する](https://qiita.com/takavfx/items/e71db089198bba1b122b)
* [Svelte で 3D 表示を行うライブラリ Threlte #3: glTF シーンを扱う](https://qiita.com/takavfx/items/aa05a313ef9e450b3731)
* [Svelte で 3D 表示を行うライブラリ Threlte #4: シーンコントロールツールセット Theatre.js for Threlte](https://qiita.com/takavfx/items/b91ba3c47e5137723a5f)
* [Svelte で3D表示を行うライブラリ Threlte #5: XR への対応](https://qiita.com/takavfx/items/6a499286c8a125c7b65d)
* Svelte で 3D 表示を行うライブラリ Threlte #6: カスタムシェーダーを適用する

# 静止シェーダーの作成と適用

## シェーダーコード

まずはシェーダーを作成する。
配置場所としては、`$lib/shaders` 以下がいいかもしれない。

```
root
  └─lib
      └─shaders
          ├─ simple.frag.glsl
          └─ simple.vert.glsl
```

```glsl:simple.vertex.glsl
varying vec3 vPosition;

void main() {
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);

  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;

  gl_Position = projectedPosition;
  vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
}
```

```glsl:simple.fragment.glsl
varying vec3 vPosition;

void main() {
  gl_FragColor = vec4(vPosition, 1.);
}
```

上記は、グローバルポジションの色だけをモデルに描画する処理。

:::note info
**ファイルの拡張子について**
実際のところ厳格にファイルの拡張子や命名は決められているわけではない。結局のところ、中身のアスキーを Raw で読み込んでパースしているだけなので。

ただ、一応の定義としては、`.glsl` か、細かく `.vert`、`.frag` などを分けられることもある。
これに従って、GLSL のハイライトサポートの恩恵も受けられるので、そこはある程度従っておくべきだろう。
しかしながら、`.glsl` の場合は、そのファイルが Vertex シェーダー用なのか、Fragment シェーダー用なのかわからなくなるため、`.vert.glsl` や `.frag.glsl` とファイル名に着けておく方が良いだろう。
:::


## 適用

シェーダーコードを読み込む際には、パスの末尾に `?raw` を付けて読み込む。

```svelte:Scene.svelte
<script lang="ts">
    import { T } from '@threlte/core';
    import fragmentShader from '$lib/shaders/simple.frag.glsl?raw';
	import vertexShader from '$lib/shaders/simple.vert.glsl?raw';
    import { ShaderMaterial } from 'three';

    const gltf = useGltf('./models/DamagedHelmet.glb', { useDraco: true });
</script>

{#if $gltf}
	<T.Mesh
		geometry={$gltf.nodes['node_damagedHelmet_-6514'].geometry}
		rotation={[1.5, 0, 0]}
		position={[0, 0, 0]}
	>
		<T.ShaderMaterial
			{fragmentShader}
			{vertexShader}
		/>
	</T.Mesh>
{/if}

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/b5098c86-2c9a-dc08-e8a3-5524cb5f43a1.png)

:::note info
その他にも、 `T.Mesh` に対して `material` を使用する方法もある。
この場合は直接 Three.js の `ShaderMaterial` を使用して、各シェーダーを渡す。

```svelte
<script lang="ts">
    // そのほか省略
    import { ShaderMaterial } from 'three';
</script>

{#if $gltf}
	<T.Mesh
		geometry={$gltf.nodes['node_damagedHelmet_-6514'].geometry}
		material={new ShaderMaterial({ vertexShader: vertexShader,
		                               fragmentShader: fragmentShader })}
		rotation={[1.5, 0, 0]}
		position={[0, 0, 0]}
	/>
{/if}
```
:::

# シェーダーアニメーションを適用する

シェーダー側に動きを点けたい場合、シェーダー外部から毎フレーム変わるユニフォームパラメーターを渡す。

サンプルとして、以下の様に、シンプルに Box だけストライプを `sin()` アニメーションに `uTime` を渡してアニメーションしてみた。

![output-palette.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/8a000f21-589a-351e-41df-8d9e23a759a2.gif)

## シェーダーコード

シェーダー側としてのポイントは Fragment シェーダー側で `uTime` と、任意の uniform パラメーターを定義して、使用しているところ。

```glsl:animLines.vert.gls
varying vec2 vUv;

void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
```

```diff_glsl:animLnies.frag..glsl
+ uniform float uTime;
uniform vec3 color;
varying vec2 vUv;
in vec2 inTexCoord;

void main() {
+ vec2 mulvUv = mod(vUv * sin(uTime) * 4.0, 1.0);
  float strength = step(0.5, mulvUv.y);
  gl_FragColor.rgba = vec4(vec3(strength), 1.0);
}
```


## 適用

適用する側で重要なのは、**タイムパラメーター処理を Threlte 環境から持ってくる** こと。
それには [`useTask`](https://threlte.xyz/docs/reference/core/use-task) を使用する
もともと [`useFrame`](https://threlte.xyz/docs/reference/core/use-frame) という機能があったが、そちらは LEGACY 扱いとなり、現在ではより拡張された [`useTask`](https://threlte.xyz/docs/reference/core/use-task) を使う用に指定されている。


```diff_svelte:Scene.svelte
<script lang="ts">
-   import { T } from '@threlte/core';
+	import { T, useTask } from '@threlte/core';
	import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras';
	import { ShaderMaterial } from 'three';

	import simpleAnimFragmentShader from '$lib/shaders/simpleAnim.fragment.glsl?raw';
	import simpleAnimVertexShader from '$lib/shaders/simpleAnim.vertex.glsl?raw';

+	let uTime = $state(0);
+	useTask(
+		(delta) => {
+			uTime += delta;
+		},
+		{ autoStart: true }
+	);
</script>

<!-- 省略 -->

<Float floatIntensity={1} floatingRange={[0, 1]}>
	<T.Mesh
		position.y={1.2}
		position.z={-0.75}
		material={new ShaderMaterial({
			vertexShader: simpleAnimVertexShader,
			fragmentShader: simpleAnimFragmentShader,
			uniforms: {
				uTime: { value: { uTime } }
			}
		})}
	>
		<T.BoxGeometry />
  </T.Mesh>
</Float>


```

:::note info
**アニメーションタイミングをコントロールする**
上記では、`useTask` の返り値を指定していないが、本来は `useTask` から `start`、`stop`、`started`、`task` を受け取ることができる。

これを利用して、例えば、ボタンを押した際にアニメーションを開始させたり、停止させたりできる。

```svelte
<script lang="ts">
    const { start, stop, started } = useFrame(
      () => {
        console.log('rendering…')
      },
      {
        autostart: false
      }
    )
    
    const toggleUseFrame = () => {
      if ($started) {
        stop()
      } else {
        start()
      }
    }
</script>
```
:::

# まとめ

静止したシェーダーに関してはそのまま適用で問題なくそのまま `<Mesh>` コンポーネントの `material` プロップに渡すなどの対応で良い。

アニメーションするシェーダーに関しては、時間情報を Svelte 側から渡してあげる様にセットアップする必要がある。