2024-10-19

React Three FiberでThree.jsに入門する

react-three-fiber
Three.js

お仕事でReact Three Fiber(以降R3Fとする)を使うことになるかもしれないので、面白そうと思って買って積んでいた初めてのThree.js 第2版 ―WebGLのためのJavaScript 3Dライブラリ―を読みながらキャッチアップしていく。

まずは1章の初めての3DシーンのサンプルをR3Fで書き直して動かしてみる。
プロジェクトはViteで作成。

https://r3f.docs.pmnd.rs/getting-started/installation#vite.js

App.tsx
import './App.css';
import { Canvas } from '@react-three/fiber';
import { Stats } from '@react-three/drei';

import { SpotLight } from 'three';
import { useRef } from 'react';
import Cube from './components/Cube';
import Sphere from './components/Sphere';
import PerspectiveCamera from './components/ PerspectiveCamera';

function App() {
  const spotLightRef = useRef<SpotLight>(null);

  return (
    <div id="canvas-container">
      <Canvas
        onCreated={(state) => {
          state.gl.setClearColor('#EEEEEE');
          state.gl.setSize(window.innerWidth, window.innerHeight);
        }}
        shadows
      >
        <Stats />
        <PerspectiveCamera
          args={[45, window.innerWidth / window.innerHeight, 0.1, 1000]}
          position={[-30, 40, 30]}
          castShadow
        />
        <axesHelper args={[20]} />
        <mesh position={[15, 0, 0]} rotation-x={-0.5 * Math.PI} receiveShadow>
          <planeGeometry args={[60, 20]} />
          <meshLambertMaterial color="#ffffff" />
        </mesh>
        <Cube position={[-4, 3, 0]} castShadow />
        <Sphere position={[20, 4, 2]} castShadow />
        <spotLight
          ref={spotLightRef}
          position={[-20, 30, -5]}
          color="#ffffff"
          intensity={2000}
          castShadow
        />
        {spotLightRef.current && (
          <cameraHelper args={[spotLightRef.current.shadow.camera]} />
        )}
      </Canvas>
    </div>
  );
}

export default App;

アニメーション等でhooksを利用したいのでコンポーネントとして切り出す

PerspectiveCamera.tsx
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
import { PerspectiveCameraProps, useFrame, useThree } from '@react-three/fiber';
import { useRef } from 'react';
import { PerspectiveCamera as ThreePerspectiveCamera } from 'three';

function PerspectiveCameraWithLookAt(props: PerspectiveCameraProps) {
  const cameraRef = useRef<ThreePerspectiveCamera | null>(null);
  const { scene } = useThree();

  useFrame(() => {
    if (cameraRef.current) {
      cameraRef.current.lookAt(scene.position);
    }
  });

  return (
    <>
      <PerspectiveCamera ref={cameraRef} makeDefault {...props} />
      <OrbitControls target={[0, 0, 0]} makeDefault />
    </>
  );
}

export default PerspectiveCameraWithLookAt;
Cube.tsx
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import { Mesh } from 'three';

function Cube(props: JSX.IntrinsicElements['mesh']) {
  const ref = useRef({} as Mesh);

  useFrame(() => {
    if (ref.current) {
      ref.current.rotation.x += 0.02;
      ref.current.rotation.y += 0.02;
      ref.current.rotation.z += 0.02;
    }
  });

  return (
    <mesh ref={ref} {...props}>
      <boxGeometry args={[4, 4, 4]} />
      <meshLambertMaterial color="#ff0000" />
    </mesh>
  );
}

export default Cube;
Sphere.tsx
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import { Mesh } from 'three';

function Sphere(props: JSX.IntrinsicElements['mesh']) {
  const ref = useRef({} as Mesh);
  let step = 0;

  useFrame(() => {
    if (ref.current) {
      step += 0.04;
      ref.current.position.x = 20 + 10 * Math.cos(step);
      ref.current.position.y = 2 + 10 * Math.abs(Math.sin(step));
    }
  });

  return (
    <mesh ref={ref} {...props}>
      <sphereGeometry args={[4, 20, 20]} />
      <meshLambertMaterial color="#7777ff" />
    </mesh>
  );
}

export default Sphere;

ざっくり理解

  • Canvasでcameraやスタイルを設定。
  • ChildrenにはSceneに追加する3Dオブジェクトを宣言的に記述できるようになっている。
  • MeshGeometryMaterialで構成される
    • Geometryはオブジェクトの形状
    • Materialはオブジェクトがどのように見えるか(このサンプルでは色やワイヤーフレームを設定)
  • axesHelperは座標ガイドでx,y,zの軸を描画してくれる
  • R3Fのコンポーネントはimport不要

補足

  • spotLightは書籍ではintensityプロパティ未設定のようだったが、設定しないと3Dオブジェクトが全て真っ暗になってしまうため2000を設定している