R3Fで自作Shader環境を整える

# はじめに

:::note warn
もうCreateReactAppは非推奨なため、以下のVite環境で構築する方がおすすめです。
:::

https://qiita.com/osakasho/items/c75d27e98aca9f4dc166

最近Shaderにさわり始めたのですが、
glslファイルを直接文字列といれるのは面倒くさく感じたので、
GLSLManagerというクラスをuseContextで使えるようにしました。

# ReactThreeFiber(R3F)で記述

## ディレクトリ構成
```
─ public
    └ glsls
        ├ testfrag,glsl
        └ testvertex.glsl

- src
    ├ App.tsx
    └ components
        ├ TestShaders.tsx
        └ GLSLManager.ts
```

## App.tsxの記述

```react
import { Canvas } from "@react-three/fiber";
import { Environment } from "@react-three/drei";
import { TestShaderComponent } from "./components/TestShaders";

return (
        <div style={{ height: "90vh" }}>
            <Canvas shadows>
                <TestShaderComponent/>
                <Environment preset="dawn" background blur={0.7} />
                <directionalLight position={[100, 100, 100]} intensity={0.8} castShadow />
            </Canvas>
        </div>
    )
```

## GLSLManager.tsの記述
```typescript
import { createContext } from "react";

export class GLSLManager {
    Ready    : boolean = false;
    dirName  : string = "./glsls/";
    fileList : string[] = [];
    GLSLList : {[key: string]: string} = {};

    constructor(){}

    /**
     * GLSLファイルをロードする
     * @param fileList 
     * @returns 
     */
    async load(fileList: string[]){
        this.Ready = false;
        this.fileList = fileList;
        await (async () => {
            await Promise.all(fileList.map(async file => {
                await fetch(this.dirName+file)
                .then(response => response.text())
                .then(text => {
                    this.GLSLList[file] = text;
                });
            }))
        })()
        this.Ready = true;
    }

    /**
     * 特定のファイルのGLSLファイルの中身を取得
     * @param fileName 
     * @returns 
     */
    getGLSLText(fileName: string): string {
        return this.GLSLList[fileName]? this.GLSLList[fileName]: null;
    }

    /**
     * 新しいGLSLファイルをセットする
     */
    async setGLSLFile(fileName: string){
        fetch(this.dirName+fileName)
          .then(response => response.text())
          .then(text => {
            this.GLSLList[fileName] = text;
        });
    }

}

export const GLSLManagerContext = createContext<GLSLManager>(null);
```

## TestShaders.tsxの記述

```react
import { OrbitControls } from "@react-three/drei";
import { useEffect, useState, useContext } from "react";
import { GLSLManager, GLSLManagerContext } from "./GLSLManager";

const Cube = () => {
    const glsl = useContext(GLSLManagerContext);
    return (
      <mesh castShadow>
        <boxGeometry args={[1, 1, 1]} />
        <shaderMaterial
          fragmentShader={glsl.getGLSLText("testfrag.glsl")}
          vertexShader={glsl.getGLSLText("testvertex.glsl")}
        />
      </mesh>
    );
};

export const TestShaderComponent = () => {

    // 読み込むGLSLファイルリスト
    const fileList: string[] = [
      "testfrag.glsl",
      "testvertex.glsl"
    ];

    const [glsl, setGLSL] = useState<GLSLManager>(null);

    useEffect(() => {
        (async () => {
            const _glsl = new GLSLManager();
            await _glsl.load(fileList);
            setGLSL(_glsl);
        })()
    }, []);

    return (
        <>
            {glsl &&
              <GLSLManagerContext.Provider value={glsl}>
                <Cube/>
              </GLSLManagerContext.Provider>
            }
            <OrbitControls/>
        </>
    )
}

```

## GLSLの中身

### testvertex.glsl
```glsl
void main() {
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  modelPosition.y += sin(modelPosition.x * 4.0) * 0.2;

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

  gl_Position = projectedPosition;
}
```

### testfrag.glsl
```glsl
varying vec2 vUv;

vec3 colorA = vec3(0.912,0.191,0.652);
vec3 colorB = vec3(1.000,0.777,0.052);

void main() {
  vec2 normalizedPixel = gl_FragCoord.xy/600.0;
  vec3 color = mix(colorA, colorB, normalizedPixel.x);

  gl_FragColor = vec4(color,1.0);
}
```

## 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493888/7148358c-1b95-176f-3620-0efc2eb2d644.png)


# おわりに

webpackでGLSLLoaderを使う方法もあるようですが、
webpackを使うほどじゃないかなと感じたので、useContextで済ませました。

今後、Yuri Artiukhさんの動画をみつつ、
ごりごり勉強していきたいなと思います。

おわり。