【React Three Fiber】Post-processingにShaderを適用する

<!-- ===================================================== -->
# 概要
<!-- ===================================================== -->
Shaderの勉強を始めて、ある程度わかったことを以下の記事にまとめました。

https://qiita.com/nemutas/items/00411f74e4ba8b54a2f4

本記事では、じゃあ実際に**React Three FiberでShaderをどのように使うのか**についてまとめました。
ただ、すべての使用ケースをまとめると長くなるので、**Post-processingにShaderを適用する**ケースに絞ってまとめています。

<!-- ==================================================== -->
# Three.jsでShaderを使用するケース
<!-- ==================================================== -->
Three.jsでは、以下のケースでShaderを使用します。（私が把握している限り）
![shader.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/c52ac75a-236a-9700-b525-110e04e1d1af.png)
※ Material Shadingは、勝手に呼んでいるだけで正式な名称ではないです。

* **<font color='#22A7F2'>Material Shading</font>**は、**MeshのMaterialに対してShaderを適用**します。ただ、Materialとは言っても、Vertex Shaderで頂点座標を変更することもできます。

* **<font color='#FD7013'>Post-processing</font>**は、レンダリングされた後の**シーン全体にShaderを適用**します。

本記事では、<font color='#FD7013'>Post-processing</font>の実装方法をまとめています。
さらに、Post-processingでは、3パターンの実装方法があります。

* **Pass**：Built-in Passを使用する方法
* **Shader**：Built-in Shaderを使用する方法
* **Custom Shader**：自分で作成したShaderを使用する方法

Material Shadingについて

https://qiita.com/nemutas/items/434806b73455634a5d27

<!-- ==================================================== -->
# Built-in Passを使用した実装
<!-- ==================================================== -->
**Built-in Pass**は、Three.js側に組み込まれているEffect Passです。内部的には、`Built-in Shader`をいい感じにチューニングして使いやすくしています。
そのため、簡単に使用することができます。

[Built-in Passの一覧](https://github.com/mrdoob/three.js/tree/dev/examples/jsm/postprocessing)
[Passを使用したサンプルの一覧](https://threejs.org/docs/index.html?q=post#examples/en/postprocessing/EffectComposer)

**React Three Fiber**では、Post-processingは**[React Postprocessing（@react-three/postprocessing）](https://docs.pmnd.rs/react-postprocessing/introduction)**としてパッケージ化されています。

Three.jsで用意されているPassとほぼ同じものがBuilt-inとして用意されていますが、若干ことなります。（おそらく内部で使用しているShaderは同じで、チューニングの仕方が少し違う感じなんだと思います）

こちらも使用は簡単にできます。サンプルはドキュメントに載っているものと載っていないものがあります。載っていないものは、Three.jsの方のサンプルを見るのが良いでしょう。

### ドキュメント
https://docs.pmnd.rs/react-postprocessing/introduction

### 成果物
https://codesandbox.io/s/post-processing-with-r3f-w0r6w

### コード
```.tsx:App.tsx
import { Stats, OrbitControls, Environment } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { Suspense, VFC } from 'react';
import { Model } from './Model';
import { PassEffects } from './component_design/PassEffects';
import { PrimitiveEffects } from './component_design/PrimitiveEffects';
import { ExtendNativeEffects } from './component_design/ExtendNativeEffects';
import { NativeEffects } from './component_design/NativeEffects';

export const App: VFC = () => {
	return (
		<div style={{ width: '100vw', height: '100vh' }}>
			<Canvas
				camera={{
					position: [0, 0, 5],
					fov: 50,
					aspect: window.innerWidth / window.innerHeight,
					near: 0.1,
					far: 2000
				}}
				dpr={window.devicePixelRatio}
				shadows>
				{/* canvas color */}
				<color attach="background" args={['#1e1e1e']} />
				{/* fps */}
				<Stats />
				{/* camera controller */}
				<OrbitControls attach="orbitControls" />
				{/* objects */}
				<Suspense fallback={null}>
					<Environment preset="forest" background />
					<Model />
				</Suspense>
				{/* Effects */}
				<PassEffects />
				{/* <PrimitiveEffects /> */}
				{/* <ExtendNativeEffects /> */}
				{/* <NativeEffects /> */}
			</Canvas>
		</div>
	);
};
```
```.tsx:PassEffects.tsx
import { useControls } from 'leva';
import React, { VFC } from 'react';
import { EffectComposer, Scanline } from '@react-three/postprocessing';

export const PassEffects: VFC = () => {
	const datas = useControls('Scanline', {
		enabled: true,
		density: { value: 1.25, min: 0, max: 10, step: 0.01 }
	});

	return (
		<EffectComposer>
			<>{datas.enabled && <Scanline density={datas.density} />}</>
		</EffectComposer>
	);
};
```
`App.tsx`では、シーンを作成しています。`{/* Effects */}`で、今後説明するEffectコンポーネントを切り替えています。

`PassEffects.tsx`では、Post-processingをシーンに適用しています。Post-processingを使用する場合、`<EffectComposer>`でPassを囲う必要があります。

```.tsx
<EffectComposer>
	<>{datas.enabled && <Scanline density={datas.density} />}</>
</EffectComposer>
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/7362ada3-7d60-2e46-852e-0c6e0533bb9e.png" width="500">

#### LEVAコントローラー
値のコントローラーには、**[LEVA](https://github.com/pmndrs/leva)**を使用しています。使い方については、以下の記事を参考にしてください。

https://qiita.com/nemutas/items/a5efdd1e8f2040f6e6e5

#### primitiveなPassを使う
**React Postprocessing**は内部的には、**[postprocessing](https://github.com/vanruesc/postprocessing)**（Three.jsのpostprocessingをパッケージ化したもの）を使用しています。
ただし、postprocessingで実装されているすべてのPassが、React Postprocessingでサポートされているわけではないようです。

[postprocessingで実装されているBuilt-in Pass](https://vanruesc.github.io/postprocessing/public/docs/identifiers.html#effects)

React Postprocessing側で実装されていない、postprocessingのPassを使用したい場合は、**[Custom effects](https://docs.pmnd.rs/react-postprocessing/effects/custom-effects)**を使用します。

```.tsx:PrimitiveEffects.tsx
import { useControls } from 'leva';
import { PixelationEffect } from 'postprocessing';
import React, { forwardRef, useMemo, VFC } from 'react';
import { EffectComposer } from '@react-three/postprocessing';

export const PrimitiveEffects: VFC = () => {
	const datas = useControls('Pixelation', {
		enabled: true,
		granularity: { value: 20, min: 0, max: 50, step: 1 }
	});

	return (
		<EffectComposer>
			<Pixelation enabled={datas.enabled} granularity={datas.granularity} />
		</EffectComposer>
	);
};

// ----------------------------------------------
type PixelationProps = {
	enabled?: boolean;
	granularity?: number;
};

const Pixelation: VFC<PixelationProps> = forwardRef((props, ref) => {
	const { enabled = true, granularity = 20 } = props;
	const effect = useMemo(() => new PixelationEffect(granularity), [
		granularity
	]);

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

:::note warn
postprocessingのPassを使用する場合、注意する点として、<strong>postprocessingはTypeScriptに対応していません。</strong>
また、型定義ファイルもnpmに公開されていません。
:::

ただ、型定義ファイルそのものを見つけ【成果物】ではそれを使用しているので、そちらを参考にしてください。
`src/types/postprocessing.d.ts`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/c4323fa4-5fe5-7c08-70df-583ccd1cd219.png" width="500">

#### GodRays Effectを使用したサンプル
https://qiita.com/nemutas/items/541fad4db8c5f3221a68

<!-- ==================================================== -->
# Built-in Shader・Custom Shaderを使用した実装
<!-- ==================================================== -->
Built-in Passは、内部的にBuilt-in Shaderを使用して、いい感じにパラメーターを調整してくれていますが、**Built-in Shaderをそのまま使う**こともできます。

[Three.jsで実装されているBuilt-in Shader](https://github.com/mrdoob/three.js/tree/master/examples/jsm/shaders)

この実装では、**Three.jsに組み込まれているBuilt-in Shader**と、**自分で用意したShader**、**Three.jsに組み込まれているBuilt-in Pass**を使う方法を紹介します。

Three.jsのサンプルの方が、React Three Fiberのサンプルより圧倒的に多く、Effect PassもThree.jsに組み込まれているものをそのまま使いたいというケースが多いと思います。

### ドキュメント
https://docs.pmnd.rs/react-three-fiber/api/additional-exports

### コード
```.tsx:ExtendNativeEffects.tsx
import { useControls } from 'leva';
import React, { useEffect, useRef, VFC } from 'react';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass';
import { PixelShader } from 'three/examples/jsm/shaders/PixelShader';
import { extend, useFrame, useThree } from '@react-three/fiber';
import { blurShader } from '../shader/blur';

extend({ EffectComposer, RenderPass, ShaderPass, GlitchPass });

export const ExtendNativeEffects: VFC = () => {
	const { gl, scene, camera, size } = useThree();
	const composerRef = useRef<EffectComposer>(null);

	const glitch_datas = useControls('Glitch', {
		enabled: false,
		goWild: false
	});

	const pixel_datas = useControls('Pixel', {
		enabled: true,
		pixelSize: { value: 10, min: 1, max: 30, step: 1 }
	});

	const blur_datas = useControls('Blur', {
		enabled: true,
		blurSize: { value: 8, min: 0, max: 20, step: 1 }
	});

	useEffect(() => {
		composerRef.current!.setSize(size.width, size.height);
	}, [size]);

	useFrame(() => {
		composerRef.current!.render();
	}, 1);

	return (
		<effectComposer ref={composerRef} args={[gl]}>
			<renderPass attachArray="passes" args={[scene, camera]} />
			{/* built-in pass */}
			<glitchPass
				attachArray="passes"
				enabled={glitch_datas.enabled}
				goWild={glitch_datas.goWild}
			/>
			{/* built-in shader */}
			<shaderPass
				attachArray="passes"
				args={[PixelShader]}
				enabled={pixel_datas.enabled}
				uniforms-resolution-value={[window.innerWidth, window.innerHeight]}
				uniforms-pixelSize-value={pixel_datas.pixelSize}
			/>
			{/* custom shader */}
			<shaderPass
				attachArray="passes"
				args={[blurShader]}
				enabled={blur_datas.enabled}
				uniforms-blurSize-value={blur_datas.blurSize}
			/>
		</effectComposer>
	);
};
```
```.ts:shader/blur.ts
import * as THREE from 'three';

export const blurShader = {
	uniforms: {
		tDiffuse: { value: null },
		resolution: {
			value: new THREE.Vector2(
				window.innerWidth,
				window.innerHeight
			).multiplyScalar(window.devicePixelRatio)
		},
		blurSize: { value: 8 }
	},
	vertexShader: `
		varying vec2 v_uv;

		void main() {
			v_uv = uv;
			gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
		}
	`,
	fragmentShader: `
		uniform sampler2D tDiffuse;
		uniform vec2 resolution;
		uniform float blurSize;

		varying vec2 v_uv;

		vec4 blur(sampler2D tex) {
			const float PI2 = 6.28318530718; // PI * 2
			const float directions = 16.0;
			const float quality = 3.0;
			
			vec2 radius = blurSize / resolution;

			// normalized pixel coordinates (0-1)
			// vec2 uv = gl_FragCoord.xy / resolution;
			vec2 uv = v_uv;

			// pixel color
			vec4 color = texture2D(tDiffuse, uv);

			int count = 1;
			for (float theta = 0.0; theta < PI2; theta += PI2 / directions) {
				vec2 dir = vec2(cos(theta), sin(theta)) * radius;

				for (float i = 1.0 / quality; i <= 1.0; i += 1.0 / quality) {
					color += texture2D(tex, uv + dir * i);
					count++;
				}
			}
			color /= float(count);

			return color;
		}

		void main() {
			gl_FragColor = blur(tDiffuse);
		}
	`
};
```

React Three Fiberの[**extend**](https://docs.pmnd.rs/react-three-fiber/api/additional-exports)を使用することで、ネイティブ（Three.js）の`Effectcomposer`や`ShaderPass`、`Built-in Pass（GlitchPass）`を、そのままコンポーネントとして扱うことができます。

```.tsx
extend({ EffectComposer, RenderPass, ShaderPass, GlitchPass });
```

#### custom shader
自分で作成したShaderを割り当てるには、以下のように記述します。

```.tsx
<shaderPass
	attachArray="passes"
	args={[blurShader]}
	enabled={blur_datas.enabled}
	uniforms-blurSize-value={blur_datas.blurSize}
/>
```
* argsには、自分で作成したShader（blurShader）を割り当てます。オブジェクト構造は、`blur.ts`を参照してください。
* `shaderPass`や`Built-in Pass（GlitchPass）`は、Passクラスを継承していて、Effectのオンオフができる**enabled**が使用できます。
* uniform変数に渡す値は、`uniforms-変数名-value`の形式で指定できます。

#### built-in shader
built-in shaderも内部的には、blurShaderと同じオブジェクト構造をしているので、shaderPassに渡すことができます。

```.tsx
<shaderPass
	attachArray="passes"
	args={[PixelShader]}
	enabled={pixel_datas.enabled}
	uniforms-resolution-value={[window.innerWidth, window.innerHeight]}
	uniforms-pixelSize-value={pixel_datas.pixelSize}
/>
```
[PixelShader](https://github.com/mrdoob/three.js/blob/master/examples/jsm/shaders/PixelShader.js)

#### built-in pass
Built-in Pass（GlitchPass）を使用するためには、型定義を行う必要があります。

EffectComposer、RenderPass、ShaderPassの型定義に関しては、**[Drei（@react-three/drei）](https://docs.pmnd.rs/drei/introduction)**をインストールすることで導入することができます。
![スクリーンショット 2021-12-15 014728.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/e833038c-c9b9-6c67-8b6c-29d9ff26edf2.png)


これらの型定義を参考にして、Built-in Pass（GlitchPass）の型定義ファイルを作成します。（余談ですが、interfaceの宣言のマージってこういうところで生きてくるんですね...:writing_hand:）

```.ts:types/built_in_effects.d.ts
import { GlitchPass } from 'three-stdlib';
import { ReactThreeFiber } from '@react-three/fiber';

declare global {
	namespace JSX {
		interface IntrinsicElements {
			glitchPass: ReactThreeFiber.Node<GlitchPass, typeof GlitchPass>;
		}
	}
}
```
あとは、`tsconfig.json`でtypesフォルダを参照してあげればOKです。

```.json:tsconfig.json
{
    "include": [
        "./src/**/*"
    ],
    "compilerOptions": {
        "strict": true,
        "esModuleInterop": true,
        "lib": [
            "dom",
            "es2015"
        ],
        "jsx": "react-jsx"
    },
    "types": [
        "types"
    ]
}
```
Built-in Pass（GlitchPass）は、Built-in Shaderをいい感じにしたものなので、直接uniformで値を渡すわけではありません。
どんな引数がとれるかは、Built-in Pass（GlitchPass）の中身を見るか、サンプルを参考にして確認しましょう。

* [Built-in Passの一覧](https://github.com/mrdoob/three.js/tree/dev/examples/jsm/postprocessing)
* [Passを使用したサンプルの一覧](https://threejs.org/docs/index.html?q=post#examples/en/postprocessing/EffectComposer)

```.tsx
<glitchPass
	attachArray="passes"
	enabled={glitch_datas.enabled}
	goWild={glitch_datas.goWild}
/>
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/c97abed1-4eee-2663-9927-f39eb0a681c1.png" width="500">

#### 参考にさせて頂いたアプリケーション
https://codesandbox.io/s/volumetric-light-w633u


<!-- ==================================================== -->
# React Postprocessingを使用したCustom Shaderの実装は...:rolling_eyes:
<!-- ==================================================== -->
Custom Shaderを使ったPost-processingは、React Postprocessingの[Custom effects](https://docs.pmnd.rs/react-postprocessing/effects/custom-effects)を使用することで実装することもできます。

が、実装がそこそこ複雑なのと、独自のGLSLの関数（mainUv、mainImage）などを実装する必要があり、Shaderを始めたばかりの自分には混乱するだけだったので、調べるのをやめました。（Custom Shaderの実装方法が他にもあったので）

mainUv、mainImageについてはドキュメントを探しても見つかりませんでした。ソースコード内だけでその存在が確認できました。
[mainUv・mainImageなどを解釈しているソースコード](https://vanruesc.github.io/postprocessing/public/docs/file/src/passes/EffectPass.js.html)

サンプルが欲しい方は、以下を参照してください。

https://codesandbox.io/s/postprocessing-custom-lens-effect-5elen

https://codesandbox.io/s/r3f-postprocessing-customeffect-rd-4w43o

<!-- ==================================================== -->
# まとめ
<!-- ==================================================== -->
React Three Fiberを使用する場合、React Postprocessingパッケージを利用することでBuilt-in Passを簡単に実装することができます。（ただし、Three.jsのBuilt-in Passとは少し異なります）

自身で作成したShaderや、Three.jsのBuilt-in Pass、Three.jsのBuilt-in Shaderを使用したい場合は、extendを使って実装できます。

Shader勉強するにあたって、その導入方法から取り掛かったのですが、思いのほか調べることが多かったです。
ネイティブ（Three.js）の情報を、じゃあラッパーパッケージ（React Three Fiber）ではどう使うのか？ということも調べる必要があるのは、ラッパーパッケージを使う宿命ですが...