Open7
react-three-fiber
基本
- 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
を使って作ることができる
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>
)
}
サンプル
- 参考
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>
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>