React Three Fiberでクリスマスツリーを作ろう

# はじめに
本日はクリスマスなので、React Three Fiber (R3F)を使って、美しく輝く3Dクリスマスツリーを作ってみましょう。

# どんなのを作るの？
以下のようなものを考えました。
- 星空を背景にした3Dクリスマスツリー
- きらめくオーナメントボール
- 落ちてくる雪の結晶
- マウスでの視点操作

# `React Three Fiber (R3F)` とは？
`React Three Fiber` は、JSの3Dライブラリ`Three.js`をReactで扱いやすくするためのライブラリです。**Reactの宣言的な記法でThree.jsを扱えるため、複雑な3Dシーンも直感的に実装**できます。

# 実装
## プロジェクトのセットアップ
まず、`Vite`を使って新しい`Reactプロジェクト`を作成します
今回は、` React + JS `を使用します
```
npm create vite@latest christmas-app --template react
cd christmas-app
```

次に、必要なパッケージをインストールします
```
npm install react-router-dom
npm install @react-three/fiber @react-three/drei 
```
## コンポーネントの実装
### 1. アプリケーションのルーティング設定
`App.jsx`でルーティングを設定し、`ChristmasHome`コンポーネントを表示します
```App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ChristmasHome from './components/ChristmasHome.jsx';

function App() {
    return (
        <Router>
            <Routes>
                <Route path="/" element={<ChristmasHome />} />
            </Routes>
        </Router>
    );
}

export default App;
```

### 2. `ChristmasHome`コンポーネントの実装
`src`に`components/ChristmasHome.jsx`を作成します。
`ChristmasHome.jsx`は以下のようなものになります。
```ChristmasHome.jsx
import React, { useRef, Suspense, useMemo } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import {
    Stars,
    OrbitControls,
    Text3D,
    Sparkles,
    Environment,
} from '@react-three/drei';

// 雪の結晶コンポーネント
function Snowflake() {
    const meshRef = useRef(null);
    const speed = Math.random() * 0.02 + 0.01;
    const rotationSpeed = Math.random() * 0.02;
    const startPosition = useMemo(() => ({
        x: Math.random() * 20 - 10,
        y: Math.random() * 10 + 5,
        z: Math.random() * 20 - 10,
    }), []);

    useFrame(() => {
        if (meshRef.current) {
            meshRef.current.position.y -= speed;
            meshRef.current.rotation.z += rotationSpeed;
            meshRef.current.rotation.x += rotationSpeed * 0.5;

            if (meshRef.current.position.y < -5) {
                meshRef.current.position.set(
                    startPosition.x,
                    startPosition.y,
                    startPosition.z
                );
            }
        }
    });

    return (
        <mesh ref={meshRef} position={[startPosition.x, startPosition.y, startPosition.z]}>
            <sphereGeometry args={[0.05, 8, 8]} />
            <meshStandardMaterial
                color="white"
                emissive="white"
                emissiveIntensity={0.2}
                transparent
                opacity={0.8}
            />
        </mesh>
    );
}

// 雪の降る演出
function Snowfall() {
    return Array.from({ length: 200 }, (_, i) => <Snowflake key={i} />);
}

// きらめくオーナメントコンポーネント
function Ornament({ position, color }) {
    return (
        <mesh position={position}>
            <sphereGeometry args={[0.15, 16, 16]} />
            <meshStandardMaterial
                color={color}
                metalness={0.8}
                roughness={0.2}
                emissive={color}
                emissiveIntensity={0.4}
            />
        </mesh>
    );
}

// クリスマスツリーコンポーネント
function ChristmasTree() {
    const treeRef = useRef();
    const colors = ['#ff0000', '#ffd700', '#00ff00', '#ff69b4', '#4169e1'];

    useFrame(({ clock }) => {
        if (treeRef.current) {
            treeRef.current.rotation.y = clock.getElapsedTime() * 0.2;
            treeRef.current.position.y = Math.sin(clock.getElapsedTime()) * 0.1 - 1.5;
        }
    });

    return (
        <group ref={treeRef} position={[0, -1.5, 0]}>
            {/* ツリーの層 */}
            {[2.5, 2, 1.5, 1].map((size, index) => (
                <mesh key={`tree-${index}`} position={[0, index * 1.2, 0]}>
                    <coneGeometry args={[size, 2, 8]} />
                    <meshStandardMaterial
                        color="#006400"
                        roughness={0.8}
                        metalness={0.2}
                    />
                </mesh>
            ))}

            {/* 幹 */}
            <mesh position={[0, -1, 0]}>
                <cylinderGeometry args={[0.3, 0.4, 1]} />
                <meshStandardMaterial color="#4a3000" />
            </mesh>

            {/* オーナメント */}
            {Array.from({ length: 30 }).map((_, i) => {
                const theta = Math.random() * Math.PI * 2;
                const y = Math.random() * 5;
                const radius = 2.5 * (1 - y / 5);
                return (
                    <Ornament
                        key={`ornament-${i}`}
                        position={[
                            radius * Math.cos(theta),
                            y - 1,
                            radius * Math.sin(theta),
                        ]}
                        color={colors[i % colors.length]}
                    />
                );
            })}

            {/* トップスター */}
            <mesh position={[0, 4.8, 0]}>
                <sphereGeometry args={[0.3, 16, 16]} />
                <meshStandardMaterial
                    color="#ffd700"
                    emissive="#ffd700"
                    emissiveIntensity={1}
                    metalness={0.9}
                    roughness={0.1}
                />
            </mesh>
        </group>
    );
}

// メインコンポーネント
function ChristmasHome() {
    return (
        <div className="fixed inset-0" style={{ height: '100svh', width:'100vw' }}>
            <Canvas camera={{ position: [0, 2, 12], fov: 60 }}>
                <color attach="background" args={['#000924']} />
                <ambientLight intensity={0.5} />
                <Suspense fallback={null}>
                    {/* 星空 */}
                    <Stars radius={100} depth={50} count={5000} factor={4} saturation={0} fade speed={1} />

                    {/* 環境ライト */}
                    <Environment preset="night" />

                    {/* 雪 */}
                    <Snowfall />

                    {/* クリスマスツリー */}
                    <ChristmasTree />

                    {/* Merry Christmas メッセージ */}
                    <Text3D font="https://threejs.org/examples/fonts/helvetiker_regular.typeface.json" size={0.8} height={0.3} position={[0, -4, 0]}>
                        Merry Christmas!
                        <meshStandardMaterial color="#FFD700" />
                    </Text3D>


                    {/* きらめきエフェクト */}
                    <Sparkles count={50} scale={[5, 5, 5]} size={5} color="gold" speed={1} />

                </Suspense>
                <OrbitControls enableZoom={false} enablePan={false} />
            </Canvas>
        </div>
    );
}

export default ChristmasHome;
```


#### `Snowflake` コンポーネント（雪の結晶）
- 雪の結晶を表現します。ランダムな位置に雪の結晶が生成され、ゆっくりと下に落ちていきます。落ちた雪の結晶は、画面の上部に戻る動きを繰り返します。
- `useFrame` を使用してアニメーションを管理。`useMemo` でランダムな初期位置を設定しています。
- `sphereGeometry`で雪を表す球を作成しています。
```jsx
// 雪の結晶コンポーネント
function Snowflake() {
    const meshRef = useRef(null);
    const speed = Math.random() * 0.02 + 0.01;
    const rotationSpeed = Math.random() * 0.02;
    const startPosition = useMemo(() => ({
        x: Math.random() * 20 - 10,
        y: Math.random() * 10 + 5,
        z: Math.random() * 20 - 10,
    }), []);

    useFrame(() => {
        if (meshRef.current) {
            meshRef.current.position.y -= speed;
            meshRef.current.rotation.z += rotationSpeed;
            meshRef.current.rotation.x += rotationSpeed * 0.5;

            if (meshRef.current.position.y < -5) {
                meshRef.current.position.set(
                    startPosition.x,
                    startPosition.y,
                    startPosition.z
                );
            }
        }
    });

    return (
        <mesh ref={meshRef} position={[startPosition.x, startPosition.y, startPosition.z]}>
            <sphereGeometry args={[0.05, 8, 8]} />
            <meshStandardMaterial
                color="white"
                emissive="white"
                emissiveIntensity={0.2}
                transparent
                opacity={0.8}
            />
        </mesh>
    );
}

```

#### 2. `Snowfall` コンポーネント（雪の降る演出）
- 200個の雪の結晶をランダムに降らせる演出を作成します。
```jsx
function Snowfall() {
    return Array.from({ length: 200 }, (_, i) => <Snowflake key={i} />);
}
```

#### 3. `Ornament` コンポーネント（きらめくオーナメント）
- 引数で与えられた場所に、引数で与えられた色の球を作成します。
```jsx
function Ornament({ position, color }) {
    return (
        <mesh position={position}>
            <sphereGeometry args={[0.15, 16, 16]} />
            <meshStandardMaterial
                color={color}
                metalness={0.8}
                roughness={0.2}
                emissive={color}
                emissiveIntensity={0.4}
            />
        </mesh>
    );
}
```

#### 4. `ChristmasTree` コンポーネント（クリスマスツリー）
クリスマスツリーを構築します。
- ツリーは`coneGeometry`を使用し、複数の円錐形を組み合わせで作成しました。
- ランダムな位置にオーナメントを配置し、ツリーの頭には星のように黄色く輝く球体をつけました。
- `useFrame`を使用してツリーはゆっくりと回転し、上下に揺れる動きを再現しました。
```jsx
function ChristmasTree() {
    const treeRef = useRef();
    const colors = ['#ff0000', '#ffd700', '#00ff00', '#ff69b4', '#4169e1'];

    useFrame(({ clock }) => {
        if (treeRef.current) {
            treeRef.current.rotation.y = clock.getElapsedTime() * 0.2;
            treeRef.current.position.y = Math.sin(clock.getElapsedTime()) * 0.1 - 1.5;
        }
    });

    return (
        <group ref={treeRef} position={[0, -1.5, 0]}>
            {[2.5, 2, 1.5, 1].map((size, index) => (
                <mesh key={`tree-${index}`} position={[0, index * 1.2, 0]}>
                    <coneGeometry args={[size, 2, 8]} />
                    <meshStandardMaterial
                        color="#006400"
                        roughness={0.8}
                        metalness={0.2}
                    />
                </mesh>
            ))}
            <mesh position={[0, -1, 0]}>
                <cylinderGeometry args={[0.3, 0.4, 1]} />
                <meshStandardMaterial color="#4a3000" />
            </mesh>
            {Array.from({ length: 30 }).map((_, i) => {
                const theta = Math.random() * Math.PI * 2;
                const y = Math.random() * 5;
                const radius = 2.5 * (1 - y / 5);
                return (
                    <Ornament
                        key={`ornament-${i}`}
                        position={[
                            radius * Math.cos(theta),
                            y - 1,
                            radius * Math.sin(theta),
                        ]}
                        color={colors[i % colors.length]}
                    />
                );
            })}
            <mesh position={[0, 4.8, 0]}>
                <sphereGeometry args={[0.3, 16, 16]} />
                <meshStandardMaterial
                    color="#ffd700"
                    emissive="#ffd700"
                    emissiveIntensity={1}
                    metalness={0.9}
                    roughness={0.1}
                />
            </mesh>
        </group>
    );
}
```

#### 5. `ChristmasHome` メインコンポーネント
3Dシーン全体を構成します。
- `Canvas` によって、シーン内のライトや背景、オブジェクト（雪、ツリー、文字、きらめきエフェクトなど）を描画します。
- `Suspense` で非同期にロードされるオブジェクトを待機します。
- `react-three/drei`の関数使用
    - `Stars`で簡単に星空を表現できるものです。
    - `Environment`でプリセットを使用して夜風の雰囲気を簡単に表現しました。
    - `Text3D`を使用して3Dテキストを表示ました。
    - `Sparkles`でクリスマスツリー周りのキラキラしたエフェクトを表現しました
```jsx
function ChristmasHome() {
    return (
        <div className="fixed inset-0" style={{ height: '100svh', width:'100vw' }}>
            <Canvas camera={{ position: [0, 2, 12], fov: 60 }}>
                <color attach="background" args={['#000924']} />
                <ambientLight intensity={0.5} />
                <Suspense fallback={null}>
                    <Stars radius={100} depth={50} count={5000} factor={4} saturation={0} fade speed={1} />
                    <Environment preset="night" />
                    <Snowfall />
                    <ChristmasTree />
                    <Text3D font="https://threejs.org/examples/fonts/helvetiker_regular.typeface.json" size={0.8} height={0.3} position={[0, -4, 0]}>
                        Merry Christmas!
                        <meshStandardMaterial color="#FFD700" />
                    </Text3D>
                    <Sparkles count={50} scale={[5, 5, 5]} size={5} color="gold" speed={1} />
                </Suspense>
                <OrbitControls enableZoom={false} enablePan={false} />
            </Canvas>
        </div>
    );
}
```

# 完成画像
![スクリーンショット 2024-12-25 14.10.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3759397/11a7c9a2-26ed-0d45-59dc-cb8cfab6dfeb.png)