React Three Fiber でホバーやクリックなどのイベントを利用する

Created on

前回は、React Three Fiber の基本から簡単にアニメーションさせるまでを紹介しました。

今回は、その続きとして、ホバーやクリックなどのイベントに反応するようにしていきます。

Three.js でそのようなイベントを利用する場合は、Raycaster1 を使って実装する必要があり少し手間がかかります。

しかし、 React Three Fiber では、コンポーネントに props を渡すだけでホバー時のエフェクトなどを追加することができます。

Props を使ってイベントに反応させる

まずはメッシュをクリックしたときに console.log() を使って clicked と表示してみましょう。

src/App.tsx
<mesh ref={meshRef} onClick={() => console.log('clicked')}>
  <sphereGeometry args={[2, 16, 16]} />
  <meshStandardMaterial color="#5B5BD6" flatShading />
</mesh>

これだけです。めちゃくちゃ簡単ですね。

ホバー時のエフェクトを追加したい場合は onPointerEnter を使います。

<mesh ref={meshRef} onPointerEnter={() => console.log('hovered')}>

console.log() だけではつまらないので、ホバー時にメッシュの見た目を変更してみましょう。

このようなケースでは、React state を一緒に利用します。

React state と組み合わせる

まずは useState をインポートし、 isActive state を用意します。

src/App.tsx
// ...
 
import { useRef, useState } from 'react';
// ...
 
function Sphere() {
  const [isActive, setIsActive] = useState(false);
 
  // ...
}

ホバー時にアクティブに、ホバーが外れたときに非アクティブになるようにします。それぞれ onPointerEnter, onPointerLeave を使います。

src/App.tsx
<mesh
  ref={meshRef}
  onPointerEnter={() => setIsActive(true)}
  onPointerLeave={() => setIsActive(false)}
>

isActive の値に応じてマテリアルの見た目が変わるようにしてみます。ここではメタリックなオレンジ色にしています。

src/App.tsx
<mesh
  ref={meshRef}
  onPointerEnter={() => setIsActive(true)}
  onPointerLeave={() => setIsActive(false)}
>
  <sphereGeometry args={[2, 16, 16]} />
  <meshStandardMaterial
    metalness={isActive ? 0.5 : 0}
    roughness={isActive ? 0.25 : 1}
    color={isActive ? '#F76B15' : '#5B5BD6'}
    flatShading
  />
</mesh>

いい感じです。

ただ、少し華やかに欠けるので、メッシュホバー時にシーンにライトが追加されるようにしていきましょう。

メッシュホバー時にシーンを更新する

現状では、<mesh /> コンポーネントが isActive state を持っていますが、シーンを更新するには <App /> コンポーネントに isActive state を持たせる必要があります。なので、この state をリフトアップします。

そして、<Sphere /> コンポーネントから isActive state を更新できるように props として setIsActive も含めて渡しておきます。

src/App.tsx
function App() {
  const [isActive, setIsActive] = useState(false);
 
  return (
    <Canvas>
      <directionalLight args={['white', 1.5]} position={[0.5, 0.5, 1]} />
      <Sphere isActive={isActive} setIsActive={setIsActive} />
    </Canvas>
  );
}

<Sphere /> コンポーネントから useState() の部分を削除し、 引数を追加します。

src/App.tsx
function Sphere() { 
  const [isActive, setIsActive] = useState(false); 
function Sphere({ 
  isActive, 
  setIsActive, 
}: { 
  isActive: boolean; 
  setIsActive: (isActive: boolean) => void; 
}) { 
  useFrame(({ clock }) => {
    if (!meshRef.current) return;
    meshRef.current.rotation.y = clock.elapsedTime * 0.3;
    meshRef.current.rotation.z = clock.elapsedTime * 0.2;
  });
  const meshRef = useRef<Mesh>(null);
 
  return (
    <mesh
      ref={meshRef}
      onPointerEnter={() => setIsActive(true)}
      onPointerLeave={() => setIsActive(false)}
    >
      <sphereGeometry args={[2, 16, 16]} />
      <meshStandardMaterial
        metalness={isActive ? 0.5 : 0}
        roughness={isActive ? 0.25 : 1}
        color={isActive ? '#F76B15' : '#5B5BD6'}
        flatShading
      />
    </mesh>
  );
}

これで、メッシュホバー時に <App /> コンポーネントの isActive state が更新されるようになります。

では、試しにひとつライトを追加してみましょう。 isActivetrue の場合に <pointLight /> を表示します。

src/App.tsx
<Canvas>
  <directionalLight args={['white', 1.5]} position={[0.5, 0.5, 1]} />
  {isActive && <pointLight intensity={30} position={[0, 3, 1]} />}
  <Sphere isActive={isActive} setIsActive={setIsActive} />
</Canvas>

少し華やかさが増しましたね!

最後に、もっとライトを追加し、ライトのアニメーションも行い、キラキラな雰囲気にしてみましょう。

まずは、useRef で利用する型 PointLight, DirectionalLight をインポートします。

src/App.tsx
import type { Mesh, PointLight, DirectionalLight } from 'three';

<DiscoLights /> コンポーネントを作成します。

src/App.tsx
function DiscoLights() {
  const lightsRef = useRef<PointLight[] | DirectionalLight[]>([]);
 
  useFrame(({ clock }) => {
    lightsRef.current.forEach((light, index) => {
      if (!light) return;
 
      const speed = 0.4 + index * 0.1;
      const offset = index * 2;
      const radius = 3 + (index % 2);
 
      if (index % 2 === 0) {
        light.position.x =
          Math.sin(clock.elapsedTime * speed + offset) * radius;
        light.position.y =
          Math.cos(clock.elapsedTime * speed + offset) * radius;
      } else {
        light.position.y =
          Math.sin(clock.elapsedTime * speed + offset) * radius;
        light.position.z =
          Math.cos(clock.elapsedTime * speed + offset) * radius;
      }
    });
  });
 
  return (
    <>
      {[
        { color: 'red', intensity: 5, type: 'point' },
        { color: 'green', intensity: 5, type: 'point' },
        { color: 'purple', intensity: 4, type: 'point' },
        { color: 'orange', intensity: 3, type: 'point' },
        { color: 'cyan', intensity: 5, type: 'point' },
        { color: 'blue', intensity: 8, type: 'directional' },
        { color: 'green', intensity: 5, type: 'directional' },
        { color: 'purple', intensity: 6, type: 'directional' },
        { color: 'red', intensity: 3, type: 'directional' },
      ].map(({ color, intensity, type }, index) =>
        type === 'point' ? (
          <pointLight
            key={`${type}-${color}`}
            ref={(el) => {
              if (el) lightsRef.current[index] = el;
            }}
            color={color}
            intensity={intensity}
          />
        ) : (
          <directionalLight
            key={`${type}-${color}`}
            ref={(el) => {
              if (el) lightsRef.current[5 + index] = el;
            }}
            color={color}
            intensity={intensity}
          />
        )
      )}
      <pointLight color="white" intensity={10} position={[0, 2, 1]} />
      <pointLight color="white" intensity={10} position={[0, -3, 1]} />
    </>
  );
}

アクティブ時のマテリアルをもっとメタリックにします。

src/App.tsx
<meshStandardMaterial
  metalness={isActive ? 0.9 : 0}
  roughness={isActive ? 0.2 : 1}
  color={isActive ? '#FFF' : '#5B5BD6'}
  flatShading
/>

<DiscoLights /><App /> に追加します。

src/App.tsx
function App() {
  const [isActive, setIsActive] = useState(false);
 
  return (
    <Canvas>
      <directionalLight args={['white', 1.5]} position={[0.5, 0.5, 1]} />
      {isActive && <DiscoLights />}
      <Sphere isActive={isActive} setIsActive={setIsActive} />
    </Canvas>
  );
}

これで、次のようになります。実際にホバーしてみてください。

キラキラですね!

さいごに

R3F では、props を使うだけでホバーやクリックなどのイベントに反応させることができるので、とてもラクですね。

React state を組み合わせて、それに応じてコンポーネントを追加したり、メッシュの見た目を変更したりするだけで、3D オブジェクトを簡単にインタラクティブにできます。

今回デモを作成するときに、本題のイベント使用部分が簡単にでき過ぎたので、無駄にメッシュをキラキラにしてみました。

このように、下準備の時間を最小限にして、演出の部分に時間を割けるというのも R3F のいいところだと感じます。

React Three Fiber、ますます魅力的です!


参考

Footnotes

  1. Raycaster – three.js docs: Raycasting is used for mouse picking (working out what objects in the 3d space the mouse is over) amongst other things.