React Three Fiberで作成したComponentをStorybookで管理する - Effect編

## 1. 概要
前回、`React Three Fiber`にて作成した3Dオブジェクトを`Storybook`上で管理する、環境構築に関する記事を書かせていただきました。

https://qiita.com/t_shi/items/fca0cd1e383dd056d076

今回は、前回の環境を用い、`Post-processing`によるエフェクト処理を`Storybook`上で管理できる様に改修しましたので、ご共有します。

## 2. 内容
**前回時点**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/9e75abce-992a-e69c-3309-8ecb02157e41.png)
**今回は`Post-processing`により、下図の様なEffectが掛かる所まで作成しました**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/6fbcecb4-a51e-75f9-9813-4254a4c8eef4.png)

`React Three Fiber`の素晴らしい所は、<ins>**`Post-processing`の様なフィルター処理ですらも`Component`として宣言的に扱える事です**</ins>
これにより、 `React`上にて、`Functional Component`を柔軟に組み合わせることにより、様々な`Scene`の作成に柔軟に再利用することができます。

### 参考記事
1. [【React Three Fiber】Post-processingにShaderを適用する](https://qiita.com/nemutas/items/72ae87d84b4cbc12601c)
2. [Code Sandboxr3f + postprocessing CustomEffect R&D
](https://codesandbox.io/s/r3f-postprocessing-customeffect-rd-4w43o?file=/effects/BadTV.js:377-630)
3. [驚くほど簡単に3Dシーンを構築！React Three Fiberを使ってみた](https://liginc.co.jp/587025)

`Storybook`を除く、コード部分等は、1,2を参考とさせていただきました。

## 3. 技術
1. レンダリング後のシーン全体にEffectを掛ける処理 = `Post-processing`を、引き続き`React Three Fiber`を用いて行いました。
2. Effect処理を宣言できるComponentを、前回同様`Storybook`にてカタログ管理できるようにしました。

コード全体は以下のgithub repositoryにて公開しています

https://github.com/tominmin/react-fiber-storybook-boilerplate


### [StoryBook](https://storybook.js.org/)とは

https://storybook.js.org/

Storybookは簡潔には、UIのカタログ作成ツールです。コンポーネントというUIのパーツ単位でデザイン、及び動作確認が可能です。


### [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction)とは

https://docs.pmnd.rs/react-three-fiber/getting-started/introduction

`javascript`上で、`webgl`ベースでの3D表現を行えるライブラリのデファクト的存在である[Three.js](https://threejs.org/)に対し、従来の手続き的な書き方でなく、`React`的な宣言的な記述にて扱える様にしたwrapperライブラリです。
```html
<Canvas>
    <!-- lightの設定 -->
    <ambientLight />
    <!-- レンダリング対象のオブジェクトの設定 -->
    <mesh>
       <boxGeometry />
       <meshStandardMaterial />
    </mesh>
    <!-- Effectの設定 -->
    <EffectComposer>
    </EffectComposer>
</Canvas>
```
今回は`<EffectComposer>`によるEffectの追加の話がメインです。

### Post-processingとは
映像処理手法の一つであり、「画面に描画する直前に、後掛けのエフェクト(フィルター)を掛ける事」という認識です
詳しくは、[Unity Documentation - Post Processing](https://docs.unity3d.com/ja/2019.4/Manual/PostProcessingOverview.html)などが参考になるかもしれません
有名どころとしては、BlurやGlitch等です

https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DGe5mv5hWlug&psig=AOvVaw1XmVr6oMAhN-qWSpiNGsCp&ust=1675671684516000&source=images&cd=vfe&ved=0CBEQjhxqFwoTCLiLms75_fwCFQAAAAAdAAAAABAE

上のthumbnail画像の様な処理 (例: Glitch処理)をレンダリング結果に掛ける事を目的としています。

### Post-processingの作成に必要なライブラリ
以下のライブラリをinstallする必要があります

#### 1. Three.jsに予め組み込まれたものを使用する場合
```shell
$ yarn add @react-three/postprocessing
```
#### 2. 自分で作成したShader (Custom Shader)を使用する場合
```shell
$ yarn add @react-three/postprocessing postprocessing
```
`postprocessing`内の`Effect`クラスを継承して使用する必要があります。

## 実装

#### 1. Three.jsに組み込まれたShaderを使用する場合
```:フォルダ構成
src
  L components/molecules/Effect
                            L PassEffect
                                    L index.tsx
                                    L index.stories.tsx
```

```jsx:src/components/molecules/PassEffect/index.tsx
import { EffectComposer, Scanline } from '@react-three/postprocessing';

type Props = {
  density?: number;
};

const PassEffect: React.FC<Props> = ({ density = 0.1 }) => {
  return (
    <EffectComposer>
      <Scanline density={density} />
    </EffectComposer>
  );
};

export default PassEffect;
```
`@react-three/postprocessing`から<ins>必要なEffect用のComponent</ins>をimportして`EffectComposer`のchildrenとして渡してあげるだけでシンプルに実現できます。

```jsx:src/components/molecules/PassEffect/index.stories.tsx
const Template: ComponentStory<typeof PassEffect> = () => {
  const datas = useControls('Scanline', {
    enabled: true,
    density: { value: 1.25, min: 0, max: 10, step: 0.01 },
  });

  return (
    <>
      <DemoScene cameraPosition={[2, 2, 2]}>
        <directionalLight color="blue" position={[3, 3, 0]}></directionalLight>
        <Box position={[0, 0, 0]} />
        {datas.enabled ? <PassEffect density={datas.density} /> : <></>}
      </DemoScene>
    </>
  );
};

```
Storybook側 (`*.stories.tsx`ファイル)からは上記の様に呼び出します。
尚、`useControls`は[leva](https://github.com/pmndrs/leva)に含まれます。下図右上の様なコントローラを表示してくれます。


#### 結果
縞目の様なEffectが掛かっていることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/10ef8513-7f5c-dd48-7609-b49d2bc46b5c.png)


#### 2. Custom Shaderを使用する場合

```:ファイル構成 (少し複雑)
src
  L components/molecules/Effect
                            L CustomEffect
                                    L index.tsx
                                    L index.stories.tsx
  L utils/shader
            L  TVNoise
                   L index.ts // shaderとcomponent間のIFを扱う
                   L fragment.ts //  フラグメントシェーダ (postprocessing)
```


呼び出し元から順に追っていくと・・・
```jsx:src/components/molecules/Effect/CustomEffect/index.stories.tsx
const Template: ComponentStory<typeof CustomEffect> = () => {
  ... // datasにはleva: useControlsの返り値が格納されている

  return (
    <>
      <DemoScene cameraPosition={[2, 2, 2]}>
        <directionalLight color="blue" position={[3, 3, 0]}></directionalLight>
        <Box position={[0, 0, 0]} />
        {datas.enabled ? (
          <CustomEffect
            distortion={datas.distortion}
            distortion2={datas.distortion2}
            speed={datas.speed}
            rollSpeed={datas.rollSpeed}
          />
        ) : (
          <></>
        )}
      </DemoScene>
    </>
  );
};
```
`CustomEffect`が自作した`Effect Component`です。
それぞれ引数を渡しています

```jsx:src/components/molecules/Effect/CustomEffect/index.tsx
import { Effect } from 'postprocessing';
import { TVNoiseShader } from '../../../../utils/shader/TVNoise';

class MyCustomEffectImpl extends Effect {
  constructor(props: EffectProps) {
    const shader = TVNoiseShader({ ...props });
    super('MyCustomEffect', shader.fragmentShader, {
      uniforms: shader.uniforms,
    });
  }
}
```

```jsx:src/components/molecules/Effect/CustomEffect/index.tsx (続き)
...

const CustomEffect = forwardRef<unknown, Props>((props, ref) => {
  const effect = useMemo(
    () =>
      new MyCustomEffectImpl({
        distortion: props.distortion,
        distortion2: props.distortion2,
        speed: props.speed,
        rollSpeed: props.rollSpeed,
      }),
    [props?.distortion, props?.distortion2, props?.speed, props?.rollSpeed]
  );

  return (
    <EffectComposer>
      <primitive ref={ref} object={effect} dispose={null} />
    </EffectComposer>
  );
});

export default CustomEffect;
```
尚、Shaderは[参考資料2](https://codesandbox.io/s/r3f-postprocessing-customeffect-rd-4w43o?file=/effects/BadTV.js:377-630)におけるShaderを参考にしています。


```jsx:src/utils/shader/TVNoise/index.ts
import { Uniform } from 'three';
import { fragmentShader } from './fragment';

...
export const TVNoiseShader = ({
  distortion = 3.0,
  distortion2 = 6.0,
  speed = 0.5,
  rollSpeed = 0.2,
}: EffectProps) => ({
  uniforms: new Map([
    ['texture', new Uniform(null)],
    ['distortion', new Uniform(distortion)],
    ['distortion2', new Uniform(distortion2)],
    ['speed', new Uniform(speed)],
    ['rollSpeed', new Uniform(rollSpeed)],
  ]),
  fragmentShader,
});
```
```jsx:src/utils/shader/TVNoise/fragment.ts
export const fragmentShader = /* glsl */ `
	// ... 略

	void mainUv(inout vec2 uv) {
		uv = vec2(...); // 略
	}
`;
```
fragmentShaderに関してはtemplate literal (文字列形式)で記載しました。

#### 結果
以下の様に、古いTVのNoiseの様な歪みが`Post-processing`にて表現される事が確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2266010/6fbcecb4-a51e-75f9-9813-4254a4c8eef4.png)


## 総括
コード全体は以下のgithub repositoryにて公開しています

https://github.com/tominmin/react-fiber-storybook-boilerplate

自作したエフェクトを`Storybook`にてカタログ管理できるのは、とても便利に感じました。
今迄作り溜めていたコードを、どんどんまとめていきたいと思いました。

一方で、`Storybook`自体の機能をいまいち使いこなしていない気もするので
今後は、`Storybook`の魅力にフォーカスした記事の作成も検討してみたいと思います。
引き続き、どうぞよろしくお願い致します。

<ins>以上</ins>