React Three Fiber でホバーやクリックなどのイベントを利用する
前回は、React Three Fiber の基本から簡単にアニメーションさせるまでを紹介しました。
今回は、その続きとして、ホバーやクリックなどのイベントに反応するようにしていきます。
Three.js でそのようなイベントを利用する場合は、Raycaster を使って実装する必要があり少し手間がかかります。
しかし、 React Three Fiber では、コンポーネントに props を渡すだけでホバー時のエフェクトなどを追加することができます。
Props を使ってイベントに反応させる
まずはメッシュをクリックしたときに console.log()
を使って clicked
と表示してみましょう。
<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 を用意します。
// ...
import { useRef, useState } from 'react';
// ...
function Sphere() {
const [isActive, setIsActive] = useState(false);
// ...
}
ホバー時にアクティブに、ホバーが外れたときに非アクティブになるようにします。それぞれ onPointerEnter
, onPointerLeave
を使います。
<mesh
ref={meshRef}
onPointerEnter={() => setIsActive(true)}
onPointerLeave={() => setIsActive(false)}
>
isActive
の値に応じてマテリアルの見た目が変わるようにしてみます。ここではメタリックなオレンジ色にしています。
<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
も含めて渡しておきます。
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()
の部分を削除し、 引数を追加します。
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 が更新されるようになります。
では、試しにひとつライトを追加してみましょう。 isActive
が true
の場合に <pointLight />
を表示します。
<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
をインポートします。
import type { Mesh, PointLight, DirectionalLight } from 'three';
<DiscoLights />
コンポーネントを作成します。
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]} />
</>
);
}
アクティブ時のマテリアルをもっとメタリックにします。
<meshStandardMaterial
metalness={isActive ? 0.9 : 0}
roughness={isActive ? 0.2 : 1}
color={isActive ? '#FFF' : '#5B5BD6'}
flatShading
/>
<DiscoLights />
を <App />
に追加します。
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、ますます魅力的です!
参考
-
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.