👻

React Three FiberとDreiについての簡単な説明記事

2024/08/20に公開

この記事について

React Three Fiber(とDrei)をThree.jsと比較しつつ、デモの解説をする記事になります。

react-three-fiberとは

ReactのComponentベースで、宣言的にThree.jsを書くことができるライブラリです。
通常のThree.jsより遅くなるということもなく、Three.jsで動作するものは全てこのライブラリを使って書くことができるそうです。(参考

デモ

公式のデモ

Three.jsで書いた場合

Three.jsで作ってみたデモ(バグあり)
バグが少しあるのとライティングが少し違いますが、こんな感じになるという雰囲気で見てください。

const scene = new THREE.Scene();

// カメラを作成
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

// レンダラーを作成
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xffffff); 
document.body.appendChild(renderer.domElement);

// ボックスジオメトリとマテリアルを作成
const geometry = new THREE.BoxGeometry();
const material1 = new THREE.MeshStandardMaterial({ color: 'orange' });
const material2 = new THREE.MeshStandardMaterial({ color: 'hotpink' });

// メッシュを作成
const mesh1 = new THREE.Mesh(geometry, material1);
mesh1.position.set(-1.2, 0, 0);
scene.add(mesh1);

const mesh2 = new THREE.Mesh(geometry, material1);
mesh2.position.set(1.2, 0, 0);
scene.add(mesh2);

// ライトを作成
const ambientLight = new THREE.AmbientLight(0x404040, Math.PI / 2);
scene.add(ambientLight);

const spotLight = new THREE.SpotLight(0xffffff, Math.PI);
spotLight.position.set(10, 10, 10);
spotLight.angle = 0.15;
spotLight.penumbra = 1;
scene.add(spotLight);

const pointLight = new THREE.PointLight(0xffffff, Math.PI);
pointLight.position.set(-10, -10, -10);
scene.add(pointLight);

// アニメーションループを設定
function animate() {
  requestAnimationFrame(animate);

  // メッシュの回転
  mesh1.rotation.x += 0.01;
  mesh2.rotation.x += 0.01;

  renderer.render(scene, camera);
}

animate();

// ウィンドウリサイズ時にレンダラーとカメラのアスペクト比を更新
window.addEventListener('resize', () => {
  const width = window.innerWidth;
  const height = window.innerHeight;
  renderer.setSize(width, height);
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
});

// メッシュのクリックイベントとホバーイベントを設定
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function onMouseMove(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects([mesh1, mesh2]);

  // すべてのメッシュの色をリセット
  mesh1.material.color.set('orange');
  mesh2.material.color.set('orange');

  // ホバーしているメッシュの色を変更
  if (intersects.length > 0) {
    intersects[0].object.material.color.set('hotpink');
  }
}

function onClick(event) {
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects([mesh1, mesh2]);

  intersects.forEach((intersect) => {
    intersect.object.scale.set(intersect.object.scale.x === 1 ? 1.5 : 1, intersect.object.scale.y === 1 ? 1.5 : 1, intersect.object.scale.z === 1 ? 1.5 : 1);
  });
}

window.addEventListener('mousemove', onMouseMove);
window.addEventListener('click', onClick);
react-three-fiberで書いた場合

参考

import * as THREE from 'three'
import { createRoot } from 'react-dom/client'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame, ThreeElements } from '@react-three/fiber'

function Box(props: ThreeElements['mesh']) {
  const meshRef = useRef<THREE.Mesh>(null!)
  const [hovered, setHover] = useState(false)
  const [active, setActive] = useState(false)
  useFrame((state, delta) => (meshRef.current.rotation.x += delta))
  return (
    <mesh
      {...props}
      ref={meshRef}
      scale={active ? 1.5 : 1}
      onClick={(event) => setActive(!active)}
      onPointerOver={(event) => setHover(true)}
      onPointerOut={(event) => setHover(false)}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  )
}

createRoot(document.getElementById('root')).render(
  <Canvas>
    <ambientLight intensity={Math.PI / 2} />
    <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
    <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
    <Box position={[-1.2, 0, 0]} />
    <Box position={[1.2, 0, 0]} />
  </Canvas>,
)

デモの解説

box部分

ここで箱型の図形のコンポーネントを定義しています。
useStateでホバー、クリック時の挙動を管理しています。
onClickonPointerOveronPointerOutで値を更新しています。
boxGeometryで大きさを定義し、meshStandardMaterial でホバー時の色の変更をしています。
useFrameは以下の通り、1フレーム事の処理を決められます。(requestAnimationFrameのようなもの)

This hook allows you to execute code on every rendered frame, like running effects, updating controls, and so on. You receive the state (same as useThree) and a clock delta. Your callback function will be invoked just before a frame is rendered. When the component unmounts it is unsubscribed automatically from the render-loop.

このフックを使うと、エフェクトの実行やコントロールの更新など、レンダリングされたフレームごとにコードを実行できます。 ステート(useThreeと同じ)とクロックデルタを受け取ります。 コールバック関数は、フレームがレンダリングされる直前に呼び出されます。 コンポーネントがアンマウントされると、レンダリングループから自動的に購読が解除されます。 / deepl翻訳

function Box(props: ThreeElements['mesh']) {
  const meshRef = useRef<THREE.Mesh>(null!)
  const [hovered, setHover] = useState(false)
  const [active, setActive] = useState(false)
  useFrame((state, delta) => (meshRef.current.rotation.x += delta))
  return (
    <mesh
      {...props}
      ref={meshRef}
      scale={active ? 1.5 : 1} // active(クリック時)に大きさを変更
      onClick={(event) => setActive(!active)} // ホバー時にuseStateの値を反転
      onPointerOver={(event) => setHover(true)} // ホバー時にuseStateをtrueに更新
      onPointerOut={(event) => setHover(false)}> // ホバー時にuseStateをfalseに更新
      <boxGeometry args={[1, 1, 1]} /> // 大きさを定義
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} /> // ホバー時に色を変更
    </mesh>
  )
}

render部分

  • ambientLight:環境光を設定
  • spotLight:スポットライトを設定
  • pointLight:ポイントライトを設定
  • Box:先ほど定義した立方体のコンポーネントを2つ配置
  • OrbitControls:カメラの操作を可能にする

Three.jsのAbmientLightのドキュメント

createRoot(document.getElementById('root')).render(
  <Canvas>
    <ambientLight intensity={Math.PI / 2} />
    <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />
    <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
    <Box position={[-1.2, 0, 0]} />
    <Box position={[1.2, 0, 0]} />
    <OrbitControls />
  </Canvas>,
)

デモはBoxをそのまま配置していますが、Reactっぽく配列をmapして表示することもできます

      {[...Array(10)].map((_, index) => {
        const angle = (index / 10) * 2 * Math.PI // 各Boxの角度
        const x = 5 * Math.cos(angle) // x座標
        const y = 5 * Math.sin(angle) // y座標
        return <Box key={index} position={[x, y, -1]} />
      })}

react-three/dreiについて

そもそもreact-three-fiberに関係するエコシステムがかなり多いです。react-three/dreiもその1つです。
dreiのgithubのREADMEにはわかりやすく記載がまとまっており、storybookまで用意されています。

作例・チュートリアルサイト紹介

参考

Discussion