【React Three Fiber】Shaderを使用してMaterialを変更する

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

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

本記事では、じゃあ実際に**React Three FiberでShaderをどのように使うのか**についてまとめました。
ただ、すべての使用ケースをまとめると長くなるので、**Materialに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='#22A7F2'>Material Shading</font>の実装方法をまとめています。
さらに、Material Shadingでは、2パターンの実装方法があります。

* **ShaderMaterial**：ShaderMaterialを使用して、Materialの特性（色、影、光沢、粗さなど）をすべて自分で実装する方法
* **Custom Material Shader**：既存のMaterial（MeshStandardMaterialなど）を使用して、Materialの特性は残しつつ特定の属性（たとえば色）のみをShaderで上書きする方法

Post-processingについて

https://qiita.com/nemutas/items/72ae87d84b4cbc12601c

<!-- ==================================================== -->
# ShaderMaterialを使用した実装
<!-- ==================================================== -->
### ドキュメント
https://threejs.org/docs/index.html?q=mate#api/en/materials/ShaderMaterial

### 成果物
https://codesandbox.io/s/shadermaterial-with-r3f-c603z

### コード
```.tsx:App.tsx
import React, { useEffect, useRef, VFC } from 'react';
import * as THREE from 'three';
import { OrbitControls, Plane, Stats } from '@react-three/drei';
import { Canvas, useThree } from '@react-three/fiber';
import { ShaderObject } from './ShaderObject';

export const App: VFC = () => {
	return (
		<div style={{ width: '100vw', height: '100vh' }}>
			<Canvas
				camera={{
					position: [-40, 40, 80],
					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" />
				{/* lights */}
				<Lights />
				{/* objects */}
				<ShaderObject />
				<Plane args={[50, 50]} position={[0, -20, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
					<meshStandardMaterial color="#fff" />
				</Plane>
				{/* helper */}
				<axesHelper />
			</Canvas>
		</div>
	);
};

const Lights: VFC = () => {
	const { scene } = useThree();
	const lightRef = useRef<THREE.DirectionalLight>(null);

	useEffect(() => {
		const helper = new THREE.CameraHelper(lightRef.current!.shadow.camera);
		scene.add(helper);
	}, [scene]);

	return (
		<>
			<ambientLight intensity={0.1} />
			<directionalLight
				ref={lightRef}
				intensity={1}
				position={[5, 30, 10]}
				castShadow
				shadow-mapSize-width={1024}
				shadow-mapSize-height={1024}
				shadow-camera-near={0.1}
				shadow-camera-far={70}
				shadow-camera-top={20}
				shadow-camera-bottom={-20}
				shadow-camera-left={-20}
				shadow-camera-right={20}
				shadow-bias={-0.003}
			/>
		</>
	);
};
```
```.tsx:ShaderObject.tsx
import { useFrame } from '@react-three/fiber';
import React, { useRef, VFC } from 'react';
import * as THREE from 'three';
import { Box } from '@react-three/drei';
import { useDepthMaterial } from './useDepthMaterial';

export const ShaderObject: VFC = () => {
	const boxRef = useRef<THREE.Mesh>(null);

	const material = new THREE.ShaderMaterial({
		uniforms: {
			u_time: { value: 0 },
			u_radius: { value: 10 }
		},
		vertexShader: vertexShader,
		fragmentShader: fragmentShader
	});

	// sync shadow
	useDepthMaterial(boxRef);

	useFrame(({ clock }) => {
		material.uniforms.u_time.value = clock.getElapsedTime();
	});

	return (
		<Box
			ref={boxRef}
			args={[10, 10, 10, 12, 12, 12]}
			material={material}
			castShadow
			receiveShadow
		/>
	);
};

const vertexShader = `
uniform float u_time;
uniform float u_radius;
varying vec2 v_uv;

float getDelta(){
	return (sin(u_time) + 1.0) / 2.0;
}

void main() {
	v_uv = uv;

	float delta = getDelta();
	vec3 v = normalize(position) * u_radius;
	vec3 pos = delta * position + (1.0 - delta) * v;

	gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;

const fragmentShader = `
#define PI 3.141592653589

uniform float u_time;
varying vec2 v_uv;

void main() {
	float noise = fract(sin(dot(v_uv.xy, vec2(12.9898, 78.233)) + u_time) * 43758.5453123);
	float r = (sin(u_time) + 1.0) / 2.0;
	float g = (sin(PI * 2.0 / 3.0 + u_time) + 1.0) / 2.0;
	float b = (sin(PI * 4.0 / 3.0 + u_time) + 1.0) / 2.0;
	vec3 color = vec3(r, g, b) * noise;

	gl_FragColor = vec4(color, 1.0);
}
`;
```
```.ts:useDepthMaterial.ts
import { useEffect, useMemo } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';

export const useDepthMaterial = (
	meshRef: React.RefObject<THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>>
) => {
	const depthMaterial = useMemo(() => {
		const depth = THREE.ShaderLib['depth'];
		return new THREE.ShaderMaterial({
			vertexShader: depthVertexShader,
			fragmentShader: depth.fragmentShader,
			uniforms: Object.assign(depth.uniforms, {
				u_time: { value: 0 },
				u_radius: { value: 10 }
			}),
			defines: {
				DEPTH_PACKING: THREE.RGBADepthPacking
			}
		});
		// console.log(depth.vertexShader)
	}, []);

	useEffect(() => {
		if (meshRef.current) {
			meshRef.current.customDepthMaterial = depthMaterial;
		}
	}, [meshRef, depthMaterial]);

	useFrame(({ clock }) => {
		depthMaterial.uniforms.u_time.value = clock.getElapsedTime();
	});
};

const depthVertexShader = `
#include <common>
#include <uv_pars_vertex>
#include <displacementmap_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
varying vec2 vHighPrecisionZW;

uniform float u_time;
uniform float u_radius;

float getDelta(){
	return (sin(u_time) + 1.0) / 2.0;
}

void main() {
	#include <uv_vertex>
	#include <skinbase_vertex>
	#ifdef USE_DISPLACEMENTMAP
		#include <beginnormal_vertex>
		#include <morphnormal_vertex>
		#include <skinnormal_vertex>
	#endif
	#include <begin_vertex>

	float delta = getDelta();
 	transformed = delta * position + (1.0 - delta) * normalize(position) * u_radius;

	#include <morphtarget_vertex>
	#include <skinning_vertex>
	#include <displacementmap_vertex>
	#include <project_vertex>
	#include <logdepthbuf_vertex>
	#include <clipping_planes_vertex>
	vHighPrecisionZW = gl_Position.zw;
}
`;
```

3つのコードブロックがあります。

* `App.tsx`は、Sceneを作成しています。（Shaderと関係ないので説明は割愛します）
* `ShaderObject.tsx`は、Material ShadingしたMeshを作成しています。
* `useDepthMaterial.ts`は、床面に落ちる影を更新するための設定をしています。

#### ShaderObject.tsx
**ShaderMaterial**の使い方はシンプルで、`uniforms`、`vertexShader`、`fragmentShader`をコンストラクタに渡すだけです。このうちvertexShader、fragmentShaderは文字列で指定します。

```.tsx
const material = new THREE.ShaderMaterial({
	uniforms: {
		u_time: { value: 0 },
		u_radius: { value: 10 }
	},
	vertexShader: vertexShader,
	fragmentShader: fragmentShader
});
```

uniformsの動的な更新は、以下のようにmaterialから行います。

```.tsx
useFrame(({ clock }) => {
	material.uniforms.u_time.value = clock.getElapsedTime();
});
```

あとは、他のmaterialのようにMeshに割り当てるだけです。

```.tsx
<Box
	ref={boxRef}
	args={[10, 10, 10, 12, 12, 12]}
	material={material}
	castShadow
	receiveShadow
/>
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/f5143b4b-aa3f-6cc2-dfe8-887bf7207c66.png" width="500">

:::note info
ライトの影響や光沢、粗さなどの情報をFragment Shaderに実装していないので、Meshにはこれらの特性がないことがわかります。
<strong>ライティングしているのに、Meshには影がない。</strong>
:::

#### useDepthMaterial.ts
Vertex Shaderで頂点座標を変更した場合、自身が受ける影（receiveShadow）は動的に更新されますが、他（床面）に落とす影（castShadow）は更新されません。
更新をするためには、Meshの**[customDepthMaterial](https://threejs.org/docs/index.html?q=objec#api/en/core/Object3D.customDepthMaterial)**を設定する必要があります。

customDepthMaterialには、depthMaterialを設定します。

* `Fragment Shader`は、組み込みShaderをそのまま使います。
* `Vertex Shader`は、`MeshのVertex Shaderで適用した形状変更`を、組み込みShaderに挿入します。
* `uniforms`も、`MeshのVertex Shaderの形状変更に必要な値`を追加します。
* `defines`では、DEPTH_PACKINGとして`RGBADepthPacking`を割り当てます。

```.tsx
const depthMaterial = useMemo(() => {
	const depth = THREE.ShaderLib['depth'];
	return new THREE.ShaderMaterial({
		vertexShader: depthVertexShader,
		fragmentShader: depth.fragmentShader,
		uniforms: Object.assign(depth.uniforms, {
			u_time: { value: 0 },
			u_radius: { value: 10 }
		}),
		defines: {
			DEPTH_PACKING: THREE.RGBADepthPacking
		}
	});
	// console.log(depth.vertexShader)
}, []);
```

useDepthMaterialでは、Vertex Shaderだけさらに記述する必要があります。実装は、上記のとおり`組み込みShaderに形状変更を挿入する形`になります。
ここで組み込みShaderがどうなっているかを知るために、コンソールに出力するのが良いです。

```.tsx
console.log(depth.vertexShader)
```

ソースコードを参照してもいいですが、使っているThree.jsのバージョンによって変わる可能性があるので、注意が必要です。（以下のリンクはr135のソースコード）
https://github.com/mrdoob/three.js/blob/r135/src/renderers/shaders/ShaderLib/depth.glsl.js

形状変更情報を追加する場合、`<begin_vertex>`を置き換えるか、その下に挿入します。

```.tsx
const depthVertexShader = `
#include <common>
#include <uv_pars_vertex>
#include <displacementmap_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
varying vec2 vHighPrecisionZW;

// ↓ -----------------------------------
uniform float u_time;
uniform float u_radius;

float getDelta(){
	return (sin(u_time) + 1.0) / 2.0;
}
// ↑ -----------------------------------

void main() {
	#include <uv_vertex>
	#include <skinbase_vertex>
	#ifdef USE_DISPLACEMENTMAP
		#include <beginnormal_vertex>
		#include <morphnormal_vertex>
		#include <skinnormal_vertex>
	#endif
	#include <begin_vertex>
	
	// ↓ -----------------------------------
	float delta = getDelta();
	transformed = delta * position + (1.0 - delta) * normalize(position) * u_radius;
	// ↑ -----------------------------------

	#include <morphtarget_vertex>
	#include <skinning_vertex>
	#include <displacementmap_vertex>
	#include <project_vertex>
	#include <logdepthbuf_vertex>
	#include <clipping_planes_vertex>
	vHighPrecisionZW = gl_Position.zw;
}
`;
```

`#include`で始まるコードは、すべて組み込みShaderです。（自前のライブラリを使用していなければ）
組み込みShaderの一覧は、以下から参照できます。
https://github.com/mrdoob/three.js/tree/master/src/renderers/shaders/ShaderChunk

##### 参考にさせて頂いた記事
[three.jsのcustomDepthMaterialを使ってみる](https://qiita.com/aa_debdeb/items/2a3527b1527380e4af85)

<!-- ==================================================== -->
# Custom Material Shaderを使用した実装
<!-- ==================================================== -->
### ドキュメント
https://threejs.org/docs/index.html?q=mesh#api/en/materials/Material.onBeforeCompile

### 成果物
https://codesandbox.io/s/custom-material-shader-with-r3f-w170k

### コード
`App.tsx`と`useDepthMaterial.ts`については、【ShaderMaterial】と同じなので割愛します。

```ShaderObject.tsx
import React, { VFC, useRef } from 'react';
import * as THREE from 'three';
import { Box } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { useDepthMaterial } from './useDepthMaterial';

export const ShaderObject: VFC = () => {
	const boxRef = useRef<THREE.Mesh>(null);

	const material = new THREE.MeshStandardMaterial({
		metalness: 0.5,
		roughness: 0.8,
		transparent: true,
		side: THREE.DoubleSide,
		wireframe: false
	});

	material.onBeforeCompile = (shader) => {
		shader.uniforms.u_time = { value: 0 };
		shader.uniforms.u_radius = { value: 10 };
		// vertex
		shader.vertexShader = vertexShaderDefine + shader.vertexShader;
		shader.vertexShader = shader.vertexShader.replace(
			'#include <beginnormal_vertex>',
			beginnormal_vertex
		);
		shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', begin_vertex);
		// fragment
		shader.fragmentShader = fragmentShaderDefine + shader.fragmentShader;
		shader.fragmentShader = shader.fragmentShader.replace(
			'#include <color_fragment>',
			color_fragment
		);

		material.userData.shader = shader;
		// debug
		// console.log('vertexShader', shader.vertexShader)
		// console.log('fragmentShader', shader.fragmentShader)
	};

	// sync shadow
	useDepthMaterial(boxRef);

	useFrame(({ clock }) => {
		const shader = material.userData.shader;
		if (shader) {
			(shader as THREE.Shader).uniforms.u_time.value = clock.getElapsedTime();
		}
	});

	return (
		<Box
			ref={boxRef}
			args={[10, 10, 10, 12, 12, 12]}
			material={material}
			castShadow
			receiveShadow
		/>
	);
};

const vertexShaderDefine = `
uniform float u_time;
uniform float u_radius;
varying vec2 v_uv;
`;

const beginnormal_vertex = `
float delta = (sin(u_time) + 1.0) / 2.0;
vec3 objectNormal = delta * normal + (1.0 - delta) * normalize(position);
`;

const begin_vertex = `
v_uv = uv;
vec3 v = normalize(position) * u_radius;
vec3 transformed = delta * position + (1.0 - delta) * v;
`;

const fragmentShaderDefine = `
uniform float u_time;
varying vec2 v_uv;
`;

const color_fragment = `
float noise = fract(sin(dot(v_uv.xy, vec2(12.9898, 78.233)) + u_time) * 43758.5453123);
float r = (sin(u_time) + 1.0) / 2.0;
float g = (sin(PI * 2.0 / 3.0 + u_time) + 1.0) / 2.0;
float b = (sin(PI * 4.0 / 3.0 + u_time) + 1.0) / 2.0;
vec3 color = vec3(r, g, b) * noise;

diffuseColor = vec4(color, 1.0);
`;
```
#### ShaderObject.tsx
既存のMaterial（例えばMeshStandardMaterial）のShaderを上書きする場合は、**[onBeforeCompile](https://threejs.org/docs/index.html?q=mesh#api/en/materials/Material.onBeforeCompile)**を使用します。

**どこをどう上書きすればいいのか**は、わたしもThree.jsやShaderの学習を始めて日が浅いのでほぼ理解できていません。
コードは、公式サンプルなどを見様見真似で書きました。

[公式サンプル](https://threejs.org/examples/#webgl_materials_modified)
[参考にさせて頂いたコード](https://jsfiddle.net/e6p0hjoa/1/)

今回わかったことは、

* 頂点座標の変更に関しては、`<beginnormal_vertex>`・`<begin_vertex>`を上書きする。
* 色の変更に関しては、`<color_fragment>`を上書きする。

ということです。ただ、ファイル名でおおよその予測はできます。

**最終的にどの変数を上書きするのか**知るためには、頑張って組み込みShaderを読むしかないです。
https://github.com/mrdoob/three.js/tree/master/src/renderers/shaders/ShaderChunk

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/2325212f-cf7d-6ea6-429f-616ee2e62e5d.png" width="500">

【ShaderMaterial】で実装したものとは違い、Mesh自体に影が落ちているのがわかります。

##### 参考にさせて頂いた記事
[three.jsのマテリアルを継承してシェーダーを追記する](https://qiita.com/watabo_shi/items/bfffed0462c5e9768687)
[three.jsのデフォルトのマテリアルを拡張する](https://biwanoie.tokyo/o2o1j/)

<!-- ==================================================== -->
# まとめ
<!-- ==================================================== -->
Shaderを使いこなせれば魔法のような表現ができますが、Three.jsの内部的な仕様まで把握しないといけないので敷居が高めです。
ただ、Three.js自体の勉強にもなるので個人的にはお得だと思います。