Open7

react-three-fiber

t12ut12u

基本

  • https://threejs-journey.com/lessons/first-r3f-application#
  • useFrame(cb: (state: RootState, delta: number) => void)で毎フレームの処理
  • const three = useThree()three.cameraとかthree.glWebGLRenderer)とかを取得できる
  • 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>
    </>
  )
}
t12ut12u

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
  }
}
t12ut12u

bufferGeometry

  • attachは、その要素を親要素にどのように適用するか
  • computeVertexNormalsは、メッシュの上面(法線)を決定する
    • これがないとdirectionalLightなどに対して陰影がつかない
  • 適宜useMemouseEffectしてく
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>
  )
}
t12ut12u

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>
)
t12ut12u

@react-three/drei

  • R3F用の便利ツール群

OrbitControls

  • makeDefaultとすると、他のコントロール(TransformControlsなど)の作動中はストップし、終わったら動かせるようになる
<OrbitControls enableDamping makeDefault />

TransformControls

  • オブジェクトのrefを渡して、3軸3平面上を動かすコントロールを表示する
  • refnull!しないと怒られる
  • modeでコントロールの種類を切り替える
const box = useRef<Mesh>(null!)

<TransformControls object={objectRef} /> {/* mode="transform" /*}
<TransformControls object={objectRef} mode="rotate" />
<TransformControls object={objectRef} mode="scale" />

PivotControls

  • TransformControlsに似ているが、anchorによってオブジェクトの相対的な位置に固定できる。
    • 下の例では、anchory1で球体のてっぺんにギズモが出る。球体のscale2になっているがギズモも一緒にスケールされる。
    • デフォルトでパースペクティブによってサイズが変わる
<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>
t12ut12u

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>
)
t12ut12u

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 />

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}
      />
  • OrthographicCameraattachするパターン
    • 型的にはいくらかマシだがコロケーションできるのがいいか
      <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>