6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

本日はクリスマスなので、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コンポーネントの実装

srccomponents/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で雪を表す球を作成しています。
// 雪の結晶コンポーネント
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個の雪の結晶をランダムに降らせる演出を作成します。
function Snowfall() {
    return Array.from({ length: 200 }, (_, i) => <Snowflake key={i} />);
}

3. Ornament コンポーネント(きらめくオーナメント)

  • 引数で与えられた場所に、引数で与えられた色の球を作成します。
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を使用してツリーはゆっくりと回転し、上下に揺れる動きを再現しました。
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でクリスマスツリー周りのキラキラしたエフェクトを表現しました
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

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?