[ReactThreeFiber]Viteで自作Shader環境を整える

# はじめに
毎回Shader環境を作成するのが面倒だったので、
R3F+Vite+Typescript+GLSLShaderテンプレート作成しました。
今回作成した成果物は以下です。
![today-shader01aa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493888/cd4ca398-368f-9b98-65f5-2c782a2a4bb7.gif)

Githubは以下です。

https://github.com/foasho/r3f-ts-shader-starter.git



# 環境構築
## Viteでプロジェクト作成
```bash
[Project name]» r3f-ts-shader-starter
[Select a framework:] » React
[Select a variant] » TypeScript + SWC
```

## 依存関係をインストール
```bash
cd r3f-ts-shader-starter
pnpm install
pnpm install three @types/three @react-three/fiber @react-three/drei
pnpm install --save-dev vite-plugin-glsl
```

## Shader環境構築(GLSL)

```ts:vite.config.ts
import { defineConfig } from 'vite'
import glsl from "vite-plugin-glsl"
import react from '@vitejs/plugin-react-swc'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), glsl(
    {
      include: [
        '**/*.glsl', '**/*.wgsl',
        '**/*.vert', '**/*.frag',
        '**/*.vs', '**/*.fs'
      ],
      exclude: undefined,          // Glob pattern, or array of glob patterns to ignore
      warnDuplicatedImports: true, // Warn if the same chunk was imported multiple times
      defaultExtension: 'glsl',    // Shader suffix when no extension is specified
      compress: false,             // Compress output shader code
      watch: true,                 // Recompile shader on change
      root: '/'                    // Directory for root imports
    }
  )],
})

```


## 起動
```bash
pnpm dev
```
**http://localhost:5173/**で確認しましょう。
![](https://storage.googleapis.com/zenn-user-upload/8cbda1f1f6a3-20230913.png)

# 実装
## WebGLのCanvasを実装
```tsx:App.tsx
import { Canvas } from "@react-three/fiber";
import { OrbitControls, GizmoHelper, GizmoViewport } from "@react-three/drei";

function App() {
  return (
    <div style={{ height: "100dvh", width: "100dvw" }}>
      <Canvas shadows>
        <ambientLight intensity={1}/>
        <pointLight position={[3, 3, 3]}/>
        <directionalLight position={[-2, 3, 5]}/>
        <mesh>
          <boxGeometry args={[1, 1, 1]}/>
          <meshStandardMaterial color="hotpink"/>
        </mesh>
        <OrbitControls/>
        <GizmoHelper alignment="top-right" margin={[75, 75]}>
          <GizmoViewport labelColor="white" axisHeadScale={1} />
        </GizmoHelper>
      </Canvas>
    </div>
  )
}

export default App
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493888/37d02fd6-3d46-70af-4b72-90ab39d826f8.png)

画面をぐりぐりするとCanvasの視点が動くはずです。

## Shaderを実装
srcフォルダ以下にglslフォルダを作成し、
hello.vertとhello.fragを作成
```diff
└ src
+  └ glsl
+     ├ hello.frag
+     └ hello.vert
```

### FragmentShaderの記述
テスト用なので、
なんかいい感じのShaderを作成します。
```glsl: hello.frag
varying vec2 vUv;
uniform float u_time;

/**
 * Line関数
 * @params uv     位置データ
 * @params speed  波の速さ
 * @params height 周波数の高さ
 * @params col    色データ
 */
vec4 Line(vec2 uv, float speed, float height, vec3 col){
  uv.y += smoothstep(1., 0., abs(uv.x)) * sin(u_time * speed + uv.x * height) * .2;
  return vec4(
    smoothstep(
      .06 * smoothstep(.2, .9, abs(uv.x)), 
      0., 
      abs(uv.y) - .004
    ) * col, 
    1.0) * smoothstep(1., .3, abs(uv.x));
}

void main() {
  vec2 uv = vUv;
  uv -= 0.5; // 中心
  uv.x *= 1.6; // [0:1.6]区画にスケール
  vec4 color = vec4(0.);
  float lattice = 5.0;
  for (float i = 0.; i <= lattice; i += 1.) {
    float lattice = 5.0;
    float t = i / lattice; // [0, 0.25, 0.50, 0.75, 1.0]
    color += Line(
      uv, 
      1. + t,
      4. + t, 
      vec3(
        .2 + t * .7, 
        .2 + t * .4, 
        0.3
      )
    );
}

  gl_FragColor = vec4(color);
}
```

### VertexShader作成
Vertexは面倒なので初期値で。
```glsl: hello.vert
varying vec2 vUv;

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

## R3F側で実装
```diff_tsx: App.tsx
+ import React from "react";
+ import { Canvas, useFrame } from "@react-three/fiber";
import { OrbitControls, GizmoHelper, GizmoViewport } from "@react-three/drei";
+ import vertexShader from "./glsl/hello.vert";
+ import fragmentShader from "./glsl/hello.frag";
+ import { ShaderMaterial } from "three";

function App() {
  return (
    <div style={{ height: "100dvh", width: "100dvw" }}>
      <Canvas shadows>
        <ambientLight intensity={1}/>
        <pointLight position={[3, 3, 3]}/>
        <directionalLight position={[-2, 3, 5]}/>
-         <mesh>
-           <boxGeometry args={[1, 1, 1]}/>
-           <meshStandardMaterial color="hotpink"/>
-         </mesh>
+         <ShaderPlane/>
        <OrbitControls/>
        <GizmoHelper alignment="top-right" margin={[75, 75]}>
          <GizmoViewport labelColor="white" axisHeadScale={1} />
        </GizmoHelper>
      </Canvas>
    </div>
  )
}

+ const ShaderPlane = () => {
+ 
+   const ref = React.useRef<ShaderMaterial>(null);
+   useFrame((_, delta) => {
+     if (ref.current) {
+       ref.current.uniforms.u_time.value += + delta;
+     }
+   });
+ 
+   return (
+     <mesh scale={3}>
+       <planeGeometry args={[1, 1, 1]}/>
+       <shaderMaterial
+         ref={ref}
+         vertexShader={vertexShader} 
+         fragmentShader={fragmentShader}
+         uniforms={{
+           u_time: { value: 0.0 }
+         }}
+      />
+     </mesh>
+  )
+ };

export default App
```

**[実行結果]**

![today-shader01aa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493888/e5eac871-c495-5cc8-5538-30ff9e84100c.gif)

# おわりに
以前書いたCRAを使ったShader環境構築は微妙なので、これからはこっちでやろうと思ってます。

PS: 本当にReactThreeFiberは楽しいです。