概要
React Three Fiberで、GodRaysエフェクトの実装方法をまとめました。
https://nemutas.github.io/r3f-godrays-effect/
GodRaysとは、光が差し込むようなエフェクトのことです。
ドキュメント・サンプル
※ ただし、公式サンプルはshaderを独自に作成しているため、実装の難易度が高いです。
3Dモデル
3Dモデルは、Sketchfabから以下のものをお借りしました。
※ 実装では、Blenderでglbファイルに変換したものを使用しています。
実装
コンポーネント毎に解説します。
FallenAngel.tsx
Canvasを作成しているコンポーネントです。コンポーネント名は、使用する3Dモデルに因んでいます。
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
を背景色として設定しています。
<color attach="background" args={['#000']} />
Model.tsx
3Dモデルを読み込むためのコンポーネントです。
1から実装しているわけではなく、雛形のコードを生成するツールを使用して、それを書き換えています。
ツールについては、以下を参照してください。
/*
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
を追加する必要があります。
const ModelPath = process.env.PUBLIC_URL + '/assets/fallen_angel.glb'
- 読み込んだ3Dモデルに影を適用している場合(receiveShadow)、モデルの表面にジャギーが発生することがあります。
これは、materialがside=DoubleSide
になっていることに起因しているようです。この場合、FrontSideを指定することで正常に表示されます。
materials['Low_chapeau1.001'].side = THREE.FrontSide
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コンポーネントでメッシュも作成しています。
<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コンポーネントで参照するようにしています。
useEffect(() => {
if (!scene.userData.refs) scene.userData.refs = {}
scene.userData.refs.lightMesh = meshRef
}, [scene.userData])
Effects.tsx
GodRaysを追加しているコンポーネントです。
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が描画されるようにします。
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は少し難しいです。
ですが、使いこなせれば表現力をぐっと高めることができると思うので、頑張って習得していきたいです。