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

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

Published Feb 16, 2025
Updated Feb 28, 2025

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

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

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

しかし、 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、ますます魅力的です!


参考

  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.