【React Three Fiber】GodRaysエフェクトの実装

<!-- ============================================================= -->
# 概要
<!-- ============================================================= -->
React Three Fiberで、**GodRays**エフェクトの実装方法をまとめました。

https://nemutas.github.io/r3f-godrays-effect/
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/3cc3392e-7847-69b1-982c-baf733ec0860.png" width="700">

:::note info
GodRaysとは、光が差し込むようなエフェクトのことです。
:::

<!-- ============================================================= -->
# ドキュメント・サンプル
<!-- ============================================================= -->

https://docs.pmnd.rs/react-postprocessing/effects/god-rays

https://codesandbox.io/s/volumetric-light-w633u

※ ただし、公式サンプルはshaderを独自に作成しているため、実装の難易度が高いです。

<!-- ============================================================= -->
# 3Dモデル
<!-- ============================================================= -->
3Dモデルは、[Sketchfab](https://sketchfab.com/)から以下のものをお借りしました。

https://skfb.ly/6SFvA

※ 実装では、Blenderでglbファイルに変換したものを使用しています。

<!-- ============================================================= -->
# 実装
<!-- ============================================================= -->
コンポーネント毎に解説します。

### FallenAngel.tsx
Canvasを作成しているコンポーネントです。コンポーネント名は、使用する3Dモデルに因んでいます。

```.tsx:FallenAngel.tsx
import React, { Suspense, VFC } from 'react';
import { OrbitControls, Stats } from '@react-three/drei';
import { Canvas } from '@react-three/fiber';
import { Effects } from './Effects';
import { Lights } from './Lights';
import { Model } from './Model';

export const FallenAngel: VFC = () => {
	return (
		<Canvas
			camera={{
				position: [0, -1.5, 3],
				fov: 50,
				aspect: window.innerWidth / window.innerHeight,
				near: 0.1,
				far: 2000
			}}
			dpr={window.devicePixelRatio}
			shadows>
			{/* canvas color */}
			<color attach="background" args={['#000']} />
			{/* fps */}
			<Stats />
			{/* camera controller */}
			<OrbitControls />
			{/* lights */}
			<Lights />
			{/* objects */}
			<Suspense fallback={null}>
				<Model position={[0, 0.3, 0]} />
			</Suspense>
			{/* effects */}
			<Effects />
			{/* helper */}
			{/* <axesHelper /> */}
		</Canvas>
	)
}
```
* GodRaysを含めた、**Postprocessing**を使用する際に気をつけるのは、**Canvasの背景色**です。

エフェクト自体がCanvas全体にかかり、背景色とブレンドして描画します。GodRaysのブレンドモードのデフォルト値は`Screen`なので、より明確に効果がわかる`#000`を背景色として設定しています。

```.tsx
<color attach="background" args={['#000']} />
```

### Model.tsx
3Dモデルを読み込むためのコンポーネントです。
1から実装しているわけではなく、雛形のコードを生成するツールを使用して、それを書き換えています。

ツールについては、以下を参照してください。

https://qiita.com/nemutas/items/f890bcc3caf0f5cbc46f#%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E9%9B%9B%E5%BD%A2%E3%81%AE%E7%94%9F%E6%88%90

```.tsx:Model.tsx
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/

import { useControls } from 'leva';
import React, { useRef, VFC } from 'react';
import * as THREE from 'three';
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import { useGLTF } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';

type GLTFResult = GLTF & {
	nodes: {
		Mesh_0001: THREE.Mesh
	}
	materials: {
		['Low_chapeau1.001']: THREE.MeshStandardMaterial
	}
}

const ModelPath = process.env.PUBLIC_URL + '/assets/fallen_angel.glb'

export const Model: VFC<JSX.IntrinsicElements['group']> = props => {
	// add controller
	const datas = useControls('model', {
		rotate: false
	})

	const group = useRef<THREE.Group>()
	const { nodes, materials } = (useGLTF(ModelPath) as unknown) as GLTFResult

	materials['Low_chapeau1.001'].side = THREE.FrontSide

	useFrame(() => {
		if (datas.rotate) {
			group.current!.rotation.y += 0.002
		}
	})

	return (
		<group ref={group} {...props} dispose={null}>
			<mesh
				castShadow
				receiveShadow
				geometry={nodes.Mesh_0001.geometry}
				material={materials['Low_chapeau1.001']}
			/>
		</group>
	)
}

useGLTF.preload(ModelPath)
```
* 今回作成したアプリケーションは、**Github Pages**にアップロードしています。

この場合、publicフォルダにある3Dモデル（fallen_angel.glb）を参照するには、`process.env.PUBLIC_URL`を追加する必要があります。

```.tsx
const ModelPath = process.env.PUBLIC_URL + '/assets/fallen_angel.glb'
```

* 読み込んだ3Dモデルに影を適用している場合（receiveShadow）、**モデルの表面にジャギーが発生する**ことがあります。

これは、materialが`side=DoubleSide`になっていることに起因しているようです。この場合、**FrontSideを指定する**ことで正常に表示されます。

```.tsx
materials['Low_chapeau1.001'].side = THREE.FrontSide
```

### Lights.tsx
ライトを設定するコンポーネントです。

```.tsx:Lights.tsx
import { useControls } from 'leva';
import React, { useEffect, useRef, VFC } from 'react';
import * as THREE from 'three';
import { useThree } from '@react-three/fiber';

export const Lights: VFC = () => {
	return (
		<>
			<ambientLight intensity={0.1} />
			<PointLight position={[0, 3, -5]} />
		</>
	)
}

type PointLightProps = {
	position: [number, number, number]
}

const PointLight: VFC<PointLightProps> = ({ position }) => {
	// add controller
	const datas = useController()

	const meshRef = useRef<THREE.Mesh>()
	const { scene } = useThree()

	useEffect(() => {
		if (!scene.userData.refs) scene.userData.refs = {}
		scene.userData.refs.lightMesh = meshRef
	}, [scene.userData])

	useEffect(() => {
		meshRef.current!.lookAt(0, 0, 0)
	}, [])

	return (
		<mesh ref={meshRef} position={position}>
			<circleGeometry args={[datas.size, 64]} />
			<meshBasicMaterial color={datas.color} side={THREE.DoubleSide} />
			<pointLight color={datas.color} intensity={1} />
		</mesh>
	)
}

const useController = () => {
	const datas = useControls('light', {
		size: {
			value: 4.5,
			min: 0.2,
			max: 10,
			step: 0.1
		},
		color: '#525252'
	})
	return datas
}
```
* GodRaysは、実際にはライトではなく**メッシュ**に適用します。

ただし、ライト（pointLight）の位置とGodRaysをかけるメッシュの位置を一致させておかないと、描画上の矛盾が発生してしまうため、Lightsコンポーネントでメッシュも作成しています。

```.tsx
<mesh ref={meshRef} position={position}>
	<circleGeometry args={[datas.size, 64]} />
	<meshBasicMaterial color={datas.color} side={THREE.DoubleSide} />
	<pointLight color={datas.color} intensity={1} />
</mesh>
```

今回は、メッシュとGodRaysを実装しているコンポーネントを分けているので、コンポーネント間でメッシュデータを渡す必要があります。

渡し方にはいくつか方法があります。

1）`sceneにuserDataを追加して渡す`
2）メッシュに名前をつけてsceneから取得する
3）recoilなどのglobal stateを使用して渡す

実装例では、メッシュ情報をsceneにuserDataとして追加して、Effectsコンポーネントで参照するようにしています。

```.tsx
useEffect(() => {
	if (!scene.userData.refs) scene.userData.refs = {}
	scene.userData.refs.lightMesh = meshRef
}, [scene.userData])
```

### Effects.tsx
GodRaysを追加しているコンポーネントです。

```.tsx
import { useControls } from 'leva';
import React, { useEffect, useState, VFC } from 'react';
import THREE from 'three';
import { useThree } from '@react-three/fiber';
import { EffectComposer, GodRays } from '@react-three/postprocessing';

export const Effects: VFC = () => {
	// add controller
	const datas = useController()

	const [lightMesh, setLightMesh] = useState<
		React.MutableRefObject<THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>>
	>()

	const { scene } = useThree()

	useEffect(() => {
		if (scene.userData.refs && scene.userData.refs.lightMesh) {
			const lightMeshRef = scene.userData.refs.lightMesh
			setLightMesh(lightMeshRef)
		}
	}, [scene.userData.refs])

	return (
		<EffectComposer>
			<>{lightMesh && datas.enabled && <GodRays sun={lightMesh.current!} {...datas} />}</>
		</EffectComposer>
	)
}

// ========================================================
const useController = () => {
	const datas = useControls('godray', {
		enabled: true,
		samples: {
			value: 100,
			min: 10,
			max: 200,
			step: 10
		},
		density: {
			value: 0.96,
			min: 0,
			max: 1,
			step: 0.01
		},
		decay: {
			value: 0.98,
			min: 0,
			max: 1,
			step: 0.01
		},
		weight: {
			value: 0.3,
			min: 0,
			max: 1,
			step: 0.01
		},
		exposure: {
			value: 1,
			min: 0,
			max: 1,
			step: 0.01
		},
		blur: {
			value: 0,
			min: 0,
			max: 1,
			step: 0.01
		}
	})
	return datas
}
```
* GodRaysを実装するには、引数に`sun`として適用させるメッシュを渡す必要があります。

メッシュは、Lightsコンポーネントで実装しuserDataとしてsceneに格納しているので、sceneから取り出して使用します。Reactの仕様上、コンポーネントのレンダリングが非同期なので、useStateを使用してuserDataがセットされたタイミングでGodRaysが描画されるようにします。

```.tsx
const [lightMesh, setLightMesh] = useState<
	React.MutableRefObject<THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>>
>()

const { scene } = useThree()

useEffect(() => {
	if (scene.userData.refs && scene.userData.refs.lightMesh) {
		const lightMeshRef = scene.userData.refs.lightMesh
		setLightMesh(lightMeshRef)
	}
}, [scene.userData.refs])

return (
	<EffectComposer>
		<>{lightMesh && datas.enabled && <GodRays sun={lightMesh.current!} {...datas} />}</>
	</EffectComposer>
)
```

* GodRaysがとる他の引数は、コントローラーで設定できるようにしています。

この値の意味は、実際にサンプルをいじって確認していただくのが早いと思います。
特に、メッシュの色の明度によっても見え方が全然違く、これらの値をうまく調整する必要があります。

公式ドキュメントの説明を載せておきます。
https://docs.pmnd.rs/react-postprocessing/effects/god-rays#props

| NAME | DESCRIPTION |
|:-|:-|
| samples | ピクセルあたりのサンプル数 |
| density | 光線の密度 |
| decay | 照明減衰係数 |
| weight | 光線の重み係数 |
| exposure | 一定の減衰係数 |
| blur | 差し込む光のぼかし具合 |


<!-- ============================================================= -->
# リポジトリ
<!-- ============================================================= -->

https://github.com/nemutas/r3f-godrays-effect

<!-- ============================================================= -->
# まとめ
<!-- ============================================================= -->
個人的に、**Postprocessing**は少し難しいです。
ですが、使いこなせれば表現力をぐっと高めることができると思うので、頑張って習得していきたいです。