3️⃣
react-three-fiber 学習メモ
はじめに
主にthree.js journeyで学習した内容のメモ。スクラップだと目次ができないので記事にします。自分用。
three.js journey はとてもよいコースです。興味があるなら受講することをおすすめします。
基本
- https://threejs-journey.com/lessons/first-r3f-application#
-
useFrame(cb: (state: RootState, delta: number) => void)
で毎フレームの処理 -
const three = useThree()
でthree.camera
とかthree.gl
(WebGLRenderer
)とかを取得できる -
const boxRef = useRef<Mesh>(null)
でメッシュに対する参照-
boxRef.current?.rotateY(delta)
で回転
-
App.tsx
import { useFrame, useThree } from '@react-three/fiber'
import React, { useRef } from 'react'
import { Group, Mesh } from 'three'
export default function App() {
const three = useThree()
const boxRef = useRef<Mesh>(null)
const groupRef = useRef<Group>(null)
useFrame((_, delta) => {
boxRef.current?.rotateY(delta)
groupRef.current?.rotateY(delta * 0.5)
})
return (
<>
<directionalLight position={[1, 2, 3]} intensity={4.5} />
<ambientLight intensity={1.5} />
<group ref={groupRef}>
<mesh position-x={-2}>
<sphereGeometry />
<meshStandardMaterial color="blue" />
</mesh>
<mesh
ref={boxRef}
position-x={2}
scale={1.5}
rotation-y={Math.PI * 0.25}
>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</group>
<mesh position-y={-1} rotation-x={-Math.PI * 0.5} scale={10}>
<planeGeometry />
<meshStandardMaterial color="greenyellow" />
</mesh>
</>
)
}
orbitControl
-
orbitControls
コンポーネントは存在しないが、extend
を使って作ることができる - 実際には
@react-three/drei
のOrbitControls
コンポーネントを使うが、一応書いておく
App.tsx
import { extend } from '@react-three/fiber'
import { OrbitControls } from 'three/examples/jsm/Addons.js'
export default function App() {
extend({ OrbitControls })
return (
<orbitControls
args={[three.camera, three.gl.domElement]}
enableDamping
dampingFactor={0.1}
rotateSpeed={0.5}
/>
)
}
- 型がなくてリンターに怒られるので追加する
global.d.ts
declare module JSX {
interface IntrinsicElements {
orbitControls: any
}
}
bufferGeometry
-
attach
は、その要素を親要素にどのように適用するか- https://docs.pmnd.rs/react-three-fiber/api/objects#dealing-with-non-scene-objects
-
bufferAttribute
は、bufferGeometry.attributes.position
に適用したいので、attach="attributes-position"
とする
-
computeVertexNormals
は、メッシュの上面(法線)を決定する- これがないと
directionalLight
などに対して陰影がつかない
- これがないと
- 適宜
useMemo
、useEffect
してく
CustomObject.tsx
import React, { useEffect, useMemo, useRef } from 'react'
import { BufferGeometry, DoubleSide } from 'three'
type Props = { count: number; size: number }
const defaultProps: Props = {
count: 10,
size: 1,
}
export default function CustomObject(props: Partial<Props> | undefined) {
const geometryRef = useRef<BufferGeometry>(null)
const verticesCount = (props?.count || defaultProps.count) * 3 // 10 triangles
const positions = useMemo(() => {
const positions = new Float32Array(verticesCount * 3) // each vertex has 3 coordinates
for (let i = 0; i < positions.length; i++) {
positions[i] = (Math.random() - 0.5) * (props?.size || defaultProps.size)
}
return positions
}, [props])
useEffect(() => {
geometryRef.current?.computeVertexNormals()
})
return (
<mesh>
<bufferGeometry ref={geometryRef}>
<bufferAttribute
attach="attributes-position"
count={verticesCount}
itemSize={3}
array={positions}
/>
</bufferGeometry>
<meshStandardMaterial color="red" side={DoubleSide} />
</mesh>
)
}
サンプル
-
index
についてはこちらがわかりやすかった -
geometry.computeVertexNormals()
を呼ぶのをすっかり忘れていたのをこちらで思い出した- ジオメトリの各面の法線を計算する関数。法線を使ってライティングに対して描画の仕方が決定されるので、これをしないと影などなく一色で塗りつぶしたみたいになる。
SlopeGeometry.tsx
import { ExtendedColors, NodeProps, Overwrite } from '@react-three/fiber'
import { BufferGeometry } from 'three'
const size = 1
const halfSize = size / 2
const p0 = [halfSize, halfSize, -halfSize]
const p1 = [-halfSize, halfSize, -halfSize]
const p2 = [halfSize, -halfSize, -halfSize]
const p3 = [-halfSize, -halfSize, -halfSize]
const p4 = [halfSize, -halfSize, halfSize]
const p5 = [-halfSize, -halfSize, halfSize]
const faces = [
[p0, p3, p1],
[p0, p2, p3],
[p0, p4, p2],
[p0, p1, p5],
[p0, p5, p4],
[p1, p3, p5],
[p2, p4, p5],
[p2, p5, p3],
]
const vertexes = new Float32Array(faces.flat(2))
// faces の p をとって flat にならべたもの
const faceIndices = new Uint16Array([
0, 3, 1, 0, 2, 3, 0, 4, 2, 0, 1, 5, 0, 5, 4, 1, 3, 5, 2, 4, 5, 2, 5, 3,
])
export default function SlopeGeometry(
props: ExtendedColors<
Overwrite<
Partial<BufferGeometry>,
NodeProps<BufferGeometry, typeof BufferGeometry>
>
>
) {
return (
<bufferGeometry {...props} onUpdate={(self) => self.computeVertexNormals()}>
<bufferAttribute
attach="attributes-position"
array={vertexes}
count={vertexes.length / 3}
itemSize={3}
/>
<bufferAttribute
attach="attributes-index"
array={faceIndices}
count={faceIndices.length}
itemSize={1}
/>
</bufferGeometry>
)
}
Canvasコンポーネント
-
camera
: カメラの設定-
position
は[x, y, z]
でいい
-
-
gl
: レンダラーの設定antialias
toneMapping
outputColorSpace
-
dpr
: Pixel Ratioの設定- デフォルトは
[1, 2]
(min 1, max 2
)で、ほとんどの場合これでいいので書かなくていい
- デフォルトは
import { Canvas } from '@react-three/fiber'
import React from 'react'
import ReactDOM from 'react-dom/client'
import { CineonToneMapping, SRGBColorSpace } from 'three'
import App from './App.tsx'
import './style.css'
const rootEl = document.querySelector('#root')
if (!rootEl) throw new Error('Root element not found')
const root = ReactDOM.createRoot(rootEl)
root.render(
<Canvas
dpr={[1, 2]} // Pixel ratio
gl={{
antialias: true,
toneMapping: CineonToneMapping,
outputColorSpace: SRGBColorSpace,
}} // WebGLRenderer props
camera={{
fov: 75,
zoom: 1,
near: 0.1,
far: 100,
position: [0, 1, 5],
}} // PerspectiveCamera props
>
<App />
</Canvas>
)
@react-three/drei
- R3F用の便利ツール群
OrbitControls
-
makeDefault
とすると、他のコントロール(TransformControls
など)の作動中はストップし、終わったら動かせるようになる
<OrbitControls enableDamping makeDefault />
TransformControls
- オブジェクトの
ref
を渡して、3軸3平面上を動かすコントロールを表示する -
ref
はnull!
しないと怒られる -
mode
でコントロールの種類を切り替える
const box = useRef<Mesh>(null!)
<TransformControls object={objectRef} /> {/* mode="transform" /*}
<TransformControls object={objectRef} mode="rotate" />
<TransformControls object={objectRef} mode="scale" />
PivotControls
-
TransformControls
に似ているが、anchor
によってオブジェクトの相対的な位置に固定できる。- 下の例では、
anchor
のy
が1
で球体のてっぺんにギズモが出る。球体のscale
が2
になっているがギズモも一緒にスケールされる。 - デフォルトでパースペクティブによってサイズが変わる
- 下の例では、
<PivotControls anchor={[0, 1, 0]} depthTest={false}>
<mesh position-x={-2} scale={2}>
<sphereGeometry />
<meshStandardMaterial color="blue" />
</mesh>
</PivotControls>
Html
- https://github.com/pmndrs/drei?tab=readme-ov-file#html
- HTMLを埋め込める。
div
タグになる。 -
center
でセンタリング-
transform: translate(-50%, -50%)
とかでもできるけど
-
-
occlude={ref[]}
で、ref
のオブジェクトの後ろにきたとき非表示になる
<Html
position={[-1, 2.5, 0]}
center
style={style.label}
occlude={[sphere, box]}
>
Hello
</Html>
Text
- https://github.com/pmndrs/drei?tab=readme-ov-file#text
- Three.js の 3D Text は機能的な限界があるのでdreiのSDFテキストを使うと良い。
-
font
も指定できる-
woff
,ttf
,otf
が指定できるが、woff
(woff2
)が軽くていい - Google Fontsからは
woff
でダウンロードできないので、変換ツールを使うか、こちらのサイトからダウンロードするといい
-
- 内部的にはtroika-three-textが使われている
<Text font="./bangers-v20-latin-regular.woff">Hello</Text>
// マテリアルの設定もできる
<Text position-y={4} position-z={-3} fontSize={4}>
Yo Yo
<meshNormalMaterial />
</Text>
Float
<Float speed={ 5 } floatIntensity={ 2 }>
<Text>Yo Yo</Text>
</Float>
MeshReflectorMaterial
<mesh position-y={-1} rotation-x={-Math.PI * 0.5} scale={10}>
<planeGeometry />
<MeshReflectorMaterial
mirror={0.5}
resolution={256}
blur={[1000, 1000]}
mixBlur={1}
/>
</mesh>
Environment
background の設定
-
onCreated
で指定するパターン
const handleCreated = ({ gl }) => {
gl.setClearColor('ivory')
}
root.render(
<Canvas
onCreated={handleCreated}
>
</Canvas>
)
-
<color attach>
を使うパターン-
color
の親要素は実際にはScene
であり、attach="background"
は親要素のbackground
プロパティにargs[0]
を設定する
-
root.render(
<Canvas>
<color args={['ivory']} attach="background" />
</Canvas>
)
Shadows
- 以下の4つを設定する
<Canvas shadows>
<light castShadow>
<mesh castShadow>
<mesh recieveShadow>
root.render(
<Canvas shadows>
<App />
</Canvas>
)
function App() {
return (
<>
<directionalLight
castShadow
shadow-mapSize={[1024, 1024]} // shadow map
/>
<mesh castShadow>{/* ... */}</mesh>
<mesh recieveShadow>{/* ... */}</mesh>
</>
)
}
<BakeShadows />
- https://github.com/pmndrs/drei?tab=readme-ov-file#bakeshadows
- 一度だけ影をレンダリングし、その後は更新しない
- オブジェクトが動かない場合はパフォーマンスを改善できる
import { BakeShadows } from '@react-three/drei'
return (
<>
<BakeShadows />
{/* ... */}
</>
)
shadow.camera
-
shadow-camera-top
などで指定するパターン- 型安全でない
<directionalLight
castShadow
shadow-camera-top={2}
shadow-camera-right={2}
shadow-camera-bottom={-2}
shadow-camera-left={-2}
/>
-
OrthographicCamera
をattach
するパターン- 型的にはいくらかマシだがコロケーションできるのがいいか
<directionalLight castShadow>
<OrthographicCamera
attach="shadow-camera"
top={5}
right={5}
bottom={-5}
left={-5}
/>
</directionalLight>
<SoftShadows />
- 投影面までの距離に応じてリアルな感じでぼやけるPCSS(Percentage-Closer Soft Shadows)をつける
- パフォーマンス影響大
- 実行中にパラメータを変えるとすべてのシェーダーが再コンパイルされるのでパフォーマンス最悪、絶対やらない
- https://github.com/pmndrs/drei?tab=readme-ov-file#softshadows
- サンプル
type SoftShadowsProps = {
/** Size of the light source (the larger the softer the light), default: 25 */
size?: number
/** Number of samples (more samples less noise but more expensive), default: 10 */
samples?: number
/** Depth focus, use it to shift the focal point (where the shadow is the sharpest), default: 0 (the beginning) */
focus?: number
}
import { SoftShadows } from '@react-three/drei'
return (
<>
<SoftShadows />
{/* ... */}
</>
)
<AccumulativeShadows />
& <RandomizedLight />
-
AccumulativeShadows
- 平面に複数の影を集積する
- z-fighting に気をつける
-
RandomizedLight
- ランダムな複数のライトを生成する
- 実質
AccumulativeShadows
と組み合わせて使う他ない
<AccumulativeShadows position-y={-0.99}>
<RandomizedLight position={[6, 8, 1]} />
</AccumulativeShadows>
<ContactShadows />
- https://github.com/pmndrs/drei?tab=readme-ov-file#contactshadows
- 平面のみ・ライトなしで使えるシャドウ
- 平面の法線方向にあるオブジェクトをそのまま平面に投影するイメージ
<ContactShadows
position={[0, -0.99, 0]) // z-fighting 対策
scale={10}
resolution={512}
far={5}
color={'#1d8f75'}
opacity={0.4}
blur={2.8}
frames={1} // 初回だけ描画する = bake
/>
Sky
- https://github.com/pmndrs/drei?tab=readme-ov-file#sky
- 太陽の位置
sunPosition
には三次元極座標Spherical
を使うといいnew Vector3().setFromSphericalCoords(r, phi, theta)
const sunPosition = new Vector3().setFromSphericalCoords(r, phi, theta)
<Sky sunPosition={sunPosition} />
Embironment Map
- https://github.com/pmndrs/drei?tab=readme-ov-file#environment
-
preset="city"
のようにプリセットも使える
<Environment
background
files={/* ... */}
environmentIntensity={2}
/>
background
を表示しない場合
-
resolution
を下げてパフォーマンスを向上させられる
<Environment preset="sunset" resolution={32} />
メッシュを光源にする場合
-
color
やmesh
を追加して、それを光源とすることもできる- 以下の場合、真っ暗な空間に大きな赤い平面が現れて、その赤色だけがシーン内を照らす
-
meshBasicMaterial
のcolor
プロパティには配列も指定できて、0~255(0~1)の範囲を超える値をRGBに指定できる
<Environment>
<color args={['black']} attach="background" />
<mesh position-z={-5} scale={10}>
<planeGeometry />
<meshBasicMaterial color={[10, 0, 0]} />
</mesh>
</Environment>
<Lightformer />
を光源にする場合
-
form
で光源の形を指定できる - それ以外は上の例と一緒
- https://codesandbox.io/p/sandbox/zealous-https-lwo219?file=/src/App.js:917-1016
<Environment>
<color args={['black']} attach="background" />
<Lightformer
position-z={-5}
scale={10}
color="red"
intensity={10}
form="ring"
/>
</Environment>
ground
- オブジェクトが地面に接地しているかのように表示する
-
height
,radius
,scale
はケースバイケースだが大体以下の値がいい感じ
-
<Environment preset="sunset" ground={{
height: 7,
radius: 28,
scale: 100
}}
/>
Stage
-
https://github.com/pmndrs/drei?tab=readme-ov-file#stage
- ヘルパーがたくさんある。詳細はStagingセクション
-
environment map
、shadows
、2つのdirectionalLight
、center
をまとめて設定する
<Stage
shadows={{ type: 'contact', opacity: 0.2, blur: 3 }}
environment="sunset"
preset="portrait"
intensity={ 6 }
>
{/* ... */}
</Stage>
Load models
モデルの読み込み
-
useLoader
またはuseGLTF
でロードして<primitive />
または<Clone />
で表示する
useLoader
を使う場合
SomeModel.tsx
import { useLoader } from '@react-three/fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
export default function SomeModel() {
const model = useLoader(
GLTFLoader,
'./models/SomeModel.gltf',
(loader) => {
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')
loader.setDRACOLoader(dracoLoader)
}
)
return <primitive object={ model.scene } />
}
App.tsx
import Placeholder from './Placeholder.tsx'
import SomeModel from './SomeModel.tsx'
import { Suspense } from 'react'
export default function App() {
return (
<Suspense
fallback={Placeholder}
>
<SomeModel />
</Suspense>
)
}
Placeholder.tsx
import { MeshProps } from '@react-three/fiber'
export default function Placeholder(props: MeshProps) {
return (
<mesh {...props}>
<boxGeometry args={[ 1, 1, 1, 2, 2, 2 ]} />
<meshBasicMaterial wireframe color="red" />
</mesh>
)
}
useGLTFを使う場合(推奨)
SomeModel.tsx
import { Clone, useGLTF } from '@react-three/drei'
export default function SomeModel() {
const model = useGLTF('./SomeModel.gltf')
return <Clone object={ model.scene } />
}
useGLTF.preload('./hamburger-draco.glb')
GLFT to component
- GLTF をコンポーネントに変換するツール
- Command line tool
- online version
Animation
AnimationAction
-
AnimationAction
に色々ヘルパーが生えている
export default function Fox() {
const fox = useGLTF('./Fox/glTF/Fox.gltf')
const animations = useAnimations(fox.animations, fox.scene)
// console.log(animations)
// actions:
// Run: (...)
// Survery: (...)
// Walk: (...)
// clips: [AnimationClip, AnimationClip, AnimationClip]
// mixer: AnimationMixier
// names: ['Survey', 'Walk', 'Run']
// ref: {current: Group}
// 走っているところからだんだん歩くように
useEffect(() => {
animations.actions.Run.play()
window.setTimeout(() => {
animations.actions.Walk.play()
animations.actions.Walk.crossFadeFrom(animations.actions.Run, 1)
}, 2000)
}, [])
}
Controlからアニメーションを切り替えるサンプル
import { useAnimations, useGLTF } from '@react-three/drei'
import { useControls } from 'leva'
import { useEffect } from 'react'
export default function Fox() {
const fox = useGLTF('./Fox/glTF/Fox.gltf')
const animations = useAnimations(fox.animations, fox.scene)
const { animationName } = useControls({
animationName: { options: animations.names },
})
useEffect(() => {
const action = animations.actions[animationName]
action?.reset().fadeIn(0.5).play()
return () => {
action?.fadeOut(0.5)
}
}, [animationName, animations.actions])
return (
<primitive
object={fox.scene}
scale={0.02}
/>
)
}
3D Text
<Text3D />
- センタリングは
- ttf -> fontName.typeface.json の変換ツール
import { Center, Text3D } from '@react-three/drei'
export default function Text() {
return (
<Center>
<Text3D
font="/path/to/font.json"
size={0.75}
height={0.2}
curveSegments={12}
bevelEnabled
bevelThickness={0.02}
bevelSize={0.02}
bevelOffset={0}
bevelSegments={5}
>
HELLO R3F
<meshNormalMaterial />
</Text3D>
</Center>
)
}
Matcap
- MatCap (Material Capture, also known as LitSphere)
- Normalに応じて変化する光源と反射を内包したテクスチャ
- カメラが動かない場合はとても使えるが、カメラが動く場合カメラではなくオブジェクトが回転しているように見える
useMatcapTexture
- https://github.com/pmndrs/drei?tab=readme-ov-file#usematcaptexture
- Loads matcap textures from this repository: https://github.com/emmelleppi/matcaps
- 第2引数に画像の幅を指定する
- 64, 128, 256, 512, 1024
import { useMatcapTexture } from '@react-three/drei'
const [matcapTexture] = useMatcapTexture('6D3B1C_895638_502A0D_844C31', 256)
// ...
<meshMatcapMaterial matcap={matcapTexture} />
パフォーマンス改善
useStateでジオメトリ・マテリアルを再利用する場合
- 記述量も多いし型も噛み合ってないので次のやり方のほうがよさそう
import { useMatcapTexture, useNormalTexture } from '@react-three/drei'
import { useState } from 'react'
import { Euler, MeshMatcapMaterial, TorusGeometry, Vector3 } from 'three'
export default function Donuts() {
const [matcapTexture] = useMatcapTexture('6D3B1C_895638_502A0D_844C31', 256)
const [normalTexture] = useNormalTexture(43)
const [geometry, setGeometry] = useState<TorusGeometry | null>()
const [material, setMaterial] = useState<MeshMatcapMaterial | null>()
return (
<>
<torusGeometry ref={setGeometry} />
<meshMatcapMaterial
ref={setMaterial}
matcap={matcapTexture}
normalMap={normalTexture}
/>
{Array.from({ length: 100 }).map((_, i) => (
<mesh
{...getRandomDonutProps()}
key={i}
material={material!}
geometry={geometry!}
/>
))}
</>
)
}
Three.jsを使ってコンポーネントの外でジオメトリ・マテリアルを作成する場合
- 単発の
useEffect
で初期化する
const torusGeometry = new TorusGeometry(1, 0.6, 16, 32)
const material = new MeshMatcapMaterial()
export default function Donuts() {
const [matcapTexture] = useMatcapTexture('6D3B1C_895638_502A0D_844C31', 256)
useEffect(() => {
matcapTexture.colorSpace = SRGBColorSpace
material.matcap = matcapTexture
material.needsUpdate = true
}, [])
}
複数のref
すべてのドーナツを回転させる
export default function Donuts() {
const donuts = useRef<(Mesh | null)[]>([])
useFrame((_, delta) => {
donuts.current.forEach((donut) => {
donut?.rotateY(delta * 0.1)
})
})
return (
<>
{Array.from({ length: 100 }).map((_, i) => (
<mesh
ref={(ref) => (donuts.current[i] = ref)}
{...getRandomDonutProps()}
key={i}
/>
))}
</>
)
}
Mouse Events
イベント一覧
<mesh
onClick={(e) => console.log('click')}
onContextMenu={(e) => console.log('context menu')}
onDoubleClick={(e) => console.log('double click')}
onWheel={(e) => console.log('wheel spins')}
onPointerUp={(e) => console.log('up')}
onPointerDown={(e) => console.log('down')}
onPointerOver={(e) => console.log('over')}
onPointerOut={(e) => console.log('out')}
onPointerEnter={(e) => console.log('enter')}
onPointerLeave={(e) => console.log('leave')}
onPointerMove={(e) => console.log('move')}
onPointerMissed={() => console.log('missed')}
onUpdate={(self) => console.log('props have been updated')}
/>
stopPropagation
- マウスイベントは手前から奥に伝播する。止めたいところで
e.stopPropagation()
する。
カーソルをポインターに
<mesh
onClick={ eventHandler }
onPointerEnter={ () => { document.body.style.cursor = 'pointer' } }
onPointerLeave={ () => { document.body.style.cursor = 'default' } }
/>
const [hovered, set] = useState()
useCursor(hovered, /*'pointer', 'auto', document.body*/)
return (
<mesh onPointerOver={() => set(true)} onPointerOut={() => set(false)}>
パフォーマンス改善
- 毎フレームテストされるイベントはなるべく避ける
- onPointerOver, onPointerEnter, onPointerOut, onPointerLeave, onPointerMove
- meshBoundsヘルパーを使って Bounding Sphere を当たり判定に使う
<mesh raycast={meshBounds} onClick={handleClick} />
-
<App />
をBVH(Bounding Volume Hierarchy)コンポーネントでラップする- Bounding Treeを自動で計算してしてくれる
- マウスイベントを使うなら脳死でつけても良さそう
import { Bvh } from '@react-three/drei'
root.render(
<Canvas camera={camera}>
<Bvh>
<App />
</Bvh>
</Canvas>
)
Discussion