4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

概要

React Three Fiberで、GodRaysエフェクトの実装方法をまとめました。

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

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

ドキュメント・サンプル

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

3Dモデル

3Dモデルは、Sketchfabから以下のものをお借りしました。

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

実装

コンポーネント毎に解説します。

FallenAngel.tsx

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

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から実装しているわけではなく、雛形のコードを生成するツールを使用して、それを書き換えています。

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

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

ライトを設定するコンポーネントです。

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 差し込む光のぼかし具合

リポジトリ

まとめ

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

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?