Next.js + React Three Fiber で美しい銀河系3Dビジュアルの作り方

Next.js + React Three Fiber で美しい銀河系3Dビジュアルの作り方

公開: 2025年7月7日
更新: 2025年7月7日
8分で読めます

はじめに

Webアプリケーションに魅力的な3D要素を追加したいと思ったことはありませんか?今回は、Next.jsとReact Three Fiberを使用して、美しい銀河系の3Dビジュアルを実装する方法を段階的に解説します。

この記事を読むことで、以下のことができるようになります:

  • Next.jsプロジェクトにReact Three Fiberを統合する方法
  • パーティクルシステムを使った銀河系の3D表現
  • パフォーマンス最適化のテクニック
  • インタラクティブな3D体験の実装

🌟 完成イメージ

最終的に、数万個のパーティクルで構成された美しい螺旋銀河を作成します。ユーザーはマウスで自由に視点を変更でき、銀河がゆっくりと回転するアニメーションを楽しむことができます。

🚀 プロジェクトセットアップ

ステップ1: Next.jsプロジェクトの作成

まず、新しいNext.jsプロジェクトを作成しましょう。

npx create-next-app@latest galaxy-3d-app --typescript --tailwind --eslint cd galaxy-3d-app

ステップ2: 必要なライブラリのインストール

3D描画に必要なライブラリをインストールします。

npm install three @react-three/fiber @react-three/drei npm install -D @types/three

ステップ3: プロジェクト構造の確認

galaxy-3d-app/
├── app/
│   ├── page.tsx
│   └── layout.tsx
├── components/
│   ├── Galaxy.tsx
│   ├── GalaxyParticles.tsx
│   └── Scene.tsx
├── lib/
│   └── utils.ts
└── package.json

🎨 基本的なCanvasの設定

ステップ4: メインページの作成

まず、メインページを設定します。Next.jsでは3Dコンテンツを適切に扱うために動的インポートを使用します。

// app/page.tsx import dynamic from 'next/dynamic' const Galaxy = dynamic(() => import('../components/Galaxy'), { ssr: false, loading: () => ( <div className="flex items-center justify-center min-h-screen bg-black text-white"> <div className="text-xl">銀河を生成中...</div> </div> ) }) export default function Home() { return ( <main className="relative w-full h-screen overflow-hidden"> <Galaxy /> {/* UI オーバーレイ */} <div className="absolute top-6 left-6 z-10 text-white"> <h1 className="text-3xl font-bold mb-2">Interactive Galaxy</h1> <p className="text-sm opacity-70">マウスで視点を変更できます</p> </div> <div className="absolute bottom-6 right-6 z-10 text-white text-sm opacity-70"> <p>React Three Fiber + Next.js</p> </div> </main> ) }

ステップ5: Galaxy コンポーネントの基本構造

// components/Galaxy.tsx 'use client' import { Canvas } from '@react-three/fiber' import { OrbitControls, Stars } from '@react-three/drei' import { Suspense } from 'react' import Scene from './Scene' export default function Galaxy() { return ( <div className="w-full h-screen"> <Canvas camera={{ position: [0, 3, 5], fov: 75, near: 0.1, far: 1000 }} dpr={[1, 2]} // デバイスピクセル比の最適化 performance={{ min: 0.5 }} // パフォーマンス調整 gl={{ antialias: true, alpha: true }} > {/* 背景の星空 */} <Stars radius={300} depth={60} count={20000} factor={7} saturation={0} /> <Suspense fallback={null}> <Scene /> </Suspense> {/* カメラコントロール */} <OrbitControls enableZoom={true} enablePan={true} enableRotate={true} minDistance={2} maxDistance={20} autoRotate={true} autoRotateSpeed={0.3} dampingFactor={0.05} enableDamping={true} /> </Canvas> </div> ) }

ステップ6: Scene コンポーネント

// components/Scene.tsx import { useRef } from 'react' import { useFrame } from '@react-three/fiber' import GalaxyParticles from './GalaxyParticles' export default function Scene() { return ( <> {/* 環境光 */} <ambientLight intensity={0.1} /> {/* ポイントライト */} <pointLight position={[0, 0, 0]} intensity={0.5} color="#ffffff" /> {/* 銀河パーティクル */} <GalaxyParticles /> </> ) }

✨ 銀河系パーティクルシステムの実装

ここからが本番です。美しい銀河系を表現するパーティクルシステムを実装しましょう。

ステップ7: GalaxyParticles コンポーネント

// components/GalaxyParticles.tsx import { useRef, useMemo } from 'react' import { useFrame } from '@react-three/fiber' import * as THREE from 'three' interface GalaxyParticlesProps { count?: number radius?: number branches?: number randomness?: number randomnessPower?: number insideColor?: string outsideColor?: string } export default function GalaxyParticles({ count = 50000, radius = 5, branches = 3, randomness = 0.2, randomnessPower = 3, insideColor = '#ff6030', outsideColor = '#1b3984' }: GalaxyParticlesProps) { const pointsRef = useRef<THREE.Points>(null) // パーティクルの位置と色を計算 const [positions, colors, scales] = useMemo(() => { const positions = new Float32Array(count * 3) const colors = new Float32Array(count * 3) const scales = new Float32Array(count) const colorInside = new THREE.Color(insideColor) const colorOutside = new THREE.Color(outsideColor) for (let i = 0; i < count; i++) { const i3 = i * 3 // 位置の計算 const radius_current = Math.random() * radius const branchAngle = (i % branches) / branches * Math.PI * 2 // 螺旋効果 const spinAngle = radius_current * 1 // ランダムネスの追加 const randomX = Math.pow(Math.random(), randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * randomness * radius_current const randomY = Math.pow(Math.random(), randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * randomness * radius_current const randomZ = Math.pow(Math.random(), randomnessPower) * (Math.random() < 0.5 ? 1 : -1) * randomness * radius_current // 最終位置 positions[i3] = Math.cos(branchAngle + spinAngle) * radius_current + randomX positions[i3 + 1] = randomY positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius_current + randomZ // 色の計算(中心から外側へのグラデーション) const mixedColor = colorInside.clone() mixedColor.lerp(colorOutside, radius_current / radius) colors[i3] = mixedColor.r colors[i3 + 1] = mixedColor.g colors[i3 + 2] = mixedColor.b // スケールの計算(距離に基づく) scales[i] = Math.random() * 0.5 + 0.5 } return [positions, colors, scales] }, [count, radius, branches, randomness, randomnessPower, insideColor, outsideColor]) // アニメーション useFrame((state, delta) => { if (pointsRef.current) { pointsRef.current.rotation.y += delta * 0.05 // 微細な上下運動 pointsRef.current.position.y = Math.sin(state.clock.elapsedTime * 0.1) * 0.1 } }) return ( <points ref={pointsRef}> <bufferGeometry> <bufferAttribute attach="attributes-position" count={count} array={positions} itemSize={3} /> <bufferAttribute attach="attributes-color" count={count} array={colors} itemSize={3} /> <bufferAttribute attach="attributes-scale" count={count} array={scales} itemSize={1} /> </bufferGeometry> <pointsMaterial size={0.008} sizeAttenuation={true} depthWrite={false} vertexColors={true} blending={THREE.AdditiveBlending} transparent={true} alphaTest={0.001} /> </points> ) }

🎛️ カスタマイズ可能なコントロール

ユーザーがリアルタイムで銀河のパラメータを調整できるコントロールパネルを追加しましょう。

ステップ8: コントロールパネル

// components/GalaxyControls.tsx 'use client' import { useState } from 'react' interface GalaxyControlsProps { onParametersChange: (params: any) => void } export default function GalaxyControls({ onParametersChange }: GalaxyControlsProps) { const [isOpen, setIsOpen] = useState(false) const [parameters, setParameters] = useState({ count: 50000, radius: 5, branches: 3, randomness: 0.2, randomnessPower: 3, insideColor: '#ff6030', outsideColor: '#1b3984' }) const handleParameterChange = (key: string, value: any) => { const newParams = { ...parameters, [key]: value } setParameters(newParams) onParametersChange(newParams) } return ( <div className="absolute top-20 left-6 z-20"> <button onClick={() => setIsOpen(!isOpen)} className="bg-white/10 text-white px-4 py-2 rounded-lg backdrop-blur-sm border border-white/20 hover:bg-white/20 transition-colors" > {isOpen ? '設定を閉じる' : '銀河設定'} </button> {isOpen && ( <div className="mt-4 bg-black/80 backdrop-blur-sm rounded-lg p-4 border border-white/20 min-w-[300px]"> <h3 className="text-white text-lg font-semibold mb-4">銀河パラメータ</h3> <div className="space-y-4 text-white"> {/* パーティクル数 */} <div> <label className="block text-sm mb-2">パーティクル数: {parameters.count.toLocaleString()}</label> <input type="range" min="10000" max="100000" step="5000" value={parameters.count} onChange={(e) => handleParameterChange('count', parseInt(e.target.value))} className="w-full" /> </div> {/* 半径 */} <div> <label className="block text-sm mb-2">半径: {parameters.radius}</label> <input type="range" min="1" max="10" step="0.1" value={parameters.radius} onChange={(e) => handleParameterChange('radius', parseFloat(e.target.value))} className="w-full" /> </div> {/* 螺旋アーム数 */} <div> <label className="block text-sm mb-2">螺旋アーム数: {parameters.branches}</label> <input type="range" min="2" max="8" step="1" value={parameters.branches} onChange={(e) => handleParameterChange('branches', parseInt(e.target.value))} className="w-full" /> </div> {/* ランダムネス */} <div> <label className="block text-sm mb-2">ランダムネス: {parameters.randomness}</label> <input type="range" min="0" max="2" step="0.01" value={parameters.randomness} onChange={(e) => handleParameterChange('randomness', parseFloat(e.target.value))} className="w-full" /> </div> {/* 色設定 */} <div className="grid grid-cols-2 gap-2"> <div> <label className="block text-sm mb-2">中心色</label> <input type="color" value={parameters.insideColor} onChange={(e) => handleParameterChange('insideColor', e.target.value)} className="w-full h-10 rounded border border-white/20" /> </div> <div> <label className="block text-sm mb-2">外側色</label> <input type="color" value={parameters.outsideColor} onChange={(e) => handleParameterChange('outsideColor', e.target.value)} className="w-full h-10 rounded border border-white/20" /> </div> </div> </div> </div> )} </div> ) }

ステップ9: コントロール付きGalaxyコンポーネントの更新

// components/Galaxy.tsx (更新版) 'use client' import { Canvas } from '@react-three/fiber' import { OrbitControls, Stars } from '@react-three/drei' import { Suspense, useState } from 'react' import Scene from './Scene' import GalaxyControls from './GalaxyControls' export default function Galaxy() { const [galaxyParams, setGalaxyParams] = useState({ count: 50000, radius: 5, branches: 3, randomness: 0.2, randomnessPower: 3, insideColor: '#ff6030', outsideColor: '#1b3984' }) return ( <div className="w-full h-screen"> <Canvas camera={{ position: [0, 3, 5], fov: 75, near: 0.1, far: 1000 }} dpr={[1, 2]} performance={{ min: 0.5 }} gl={{ antialias: true, alpha: true }} > <color attach="background" args={['#020617']} /> <Stars radius={300} depth={60} count={20000} factor={7} saturation={0} /> <Suspense fallback={null}> <Scene galaxyParams={galaxyParams} /> </Suspense> <OrbitControls enableZoom={true} enablePan={true} enableRotate={true} minDistance={2} maxDistance={20} autoRotate={true} autoRotateSpeed={0.3} dampingFactor={0.05} enableDamping={true} /> </Canvas> <GalaxyControls onParametersChange={setGalaxyParams} /> </div> ) }

ステップ10: Scene コンポーネントの更新

// components/Scene.tsx (更新版) import GalaxyParticles from './GalaxyParticles' interface SceneProps { galaxyParams: any } export default function Scene({ galaxyParams }: SceneProps) { return ( <> <ambientLight intensity={0.1} /> <pointLight position={[0, 0, 0]} intensity={0.5} color="#ffffff" /> <GalaxyParticles {...galaxyParams} /> </> ) }

🚀 パフォーマンス最適化

ステップ11: 動的品質調整

// lib/performanceOptimizer.ts export class PerformanceOptimizer { private frameCount = 0 private lastTime = performance.now() private fps = 60 private targetFPS = 30 update(): boolean { this.frameCount++ const currentTime = performance.now() if (currentTime - this.lastTime >= 1000) { this.fps = this.frameCount this.frameCount = 0 this.lastTime = currentTime } return this.fps < this.targetFPS } getFPS(): number { return this.fps } getOptimalParticleCount(baseCount: number): number { if (this.fps < 20) { return Math.max(baseCount * 0.5, 10000) } else if (this.fps < 30) { return Math.max(baseCount * 0.75, 20000) } return baseCount } }

ステップ12: レスポンシブ対応とモバイル最適化

// hooks/useResponsiveGalaxy.ts import { useState, useEffect } from 'react' export function useResponsiveGalaxy() { const [isMobile, setIsMobile] = useState(false) const [particleCount, setParticleCount] = useState(50000) useEffect(() => { const checkDevice = () => { const mobile = window.innerWidth < 768 || /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) setIsMobile(mobile) setParticleCount(mobile ? 15000 : 50000) } checkDevice() window.addEventListener('resize', checkDevice) return () => window.removeEventListener('resize', checkDevice) }, []) return { isMobile, particleCount } }

ステップ13: WebGL サポートチェック

// components/WebGLChecker.tsx 'use client' import { useEffect, useState } from 'react' function checkWebGLSupport(): boolean { try { const canvas = document.createElement('canvas') return !!( window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')) ) } catch (e) { return false } } interface WebGLCheckerProps { children: React.ReactNode } export default function WebGLChecker({ children }: WebGLCheckerProps) { const [isSupported, setIsSupported] = useState<boolean | null>(null) useEffect(() => { setIsSupported(checkWebGLSupport()) }, []) if (isSupported === null) { return ( <div className="flex items-center justify-center min-h-screen bg-black text-white"> <div className="text-xl">WebGL サポートを確認中...</div> </div> ) } if (!isSupported) { return ( <div className="flex items-center justify-center min-h-screen bg-black text-white p-8"> <div className="text-center max-w-lg"> <h2 className="text-2xl font-bold mb-4">WebGL 非対応</h2> <p className="text-gray-300 mb-4"> お使いのブラウザはWebGLをサポートしていないため、3Dコンテンツを表示できません。 </p> <p className="text-sm text-gray-400"> 最新版のChrome、Firefox、Safari、またはEdgeをお使いください。 </p> </div> </div> ) } return <>{children}</> }

ステップ14: 最終的なページ構成

// app/page.tsx (最終版) import dynamic from 'next/dynamic' import WebGLChecker from '../components/WebGLChecker' const Galaxy = dynamic(() => import('../components/Galaxy'), { ssr: false, loading: () => ( <div className="flex items-center justify-center min-h-screen bg-black text-white"> <div className="text-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mb-4 mx-auto"></div> <div className="text-xl">銀河を生成中...</div> </div> </div> ) }) export default function Home() { return ( <WebGLChecker> <main className="relative w-full h-screen overflow-hidden"> <Galaxy /> <div className="absolute top-6 left-6 z-10 text-white"> <h1 className="text-3xl font-bold mb-2">Interactive Galaxy</h1> <p className="text-sm opacity-70">マウスドラッグで視点変更・ホイールでズーム</p> </div> <div className="absolute bottom-6 right-6 z-10 text-white text-sm opacity-70"> <p>Built with React Three Fiber + Next.js</p> </div> </main> </WebGLChecker> ) }

🎉 追加機能とカスタマイズ

パーティクルアニメーション

より動的な銀河を作るために、パーティクルに個別のアニメーションを追加することもできます:

// components/AnimatedGalaxyParticles.tsx useFrame((state, delta) => { if (pointsRef.current) { // 基本回転 pointsRef.current.rotation.y += delta * 0.05 // パーティクルの脈動効果 const positions = pointsRef.current.geometry.attributes.position.array as Float32Array for (let i = 0; i < positions.length; i += 3) { const distance = Math.sqrt(positions[i] ** 2 + positions[i + 2] ** 2) const pulse = Math.sin(state.clock.elapsedTime * 2 + distance * 0.5) * 0.01 positions[i + 1] += pulse } pointsRef.current.geometry.attributes.position.needsUpdate = true } })

プリセット銀河

異なる銀河タイプのプリセットを追加:

export const galaxyPresets = { spiral: { branches: 3, randomness: 0.2, insideColor: '#ff6030', outsideColor: '#1b3984' }, elliptical: { branches: 1, randomness: 0.8, insideColor: '#ffaa00', outsideColor: '#ff3300' }, irregular: { branches: 5, randomness: 1.2, insideColor: '#00ffaa', outsideColor: '#0066ff' }, barred: { branches: 2, randomness: 0.1, insideColor: '#ff00aa', outsideColor: '#6600ff' } }

📱 デプロイと最適化

Vercel への デプロイ

# プロダクションビルド npm run build # Vercel CLI でデプロイ npx vercel # または GitHub 連携でオートデプロイ

next.config.js の最適化

/** @type {import('next').NextConfig} */ const nextConfig = { experimental: { optimizePackageImports: ['three', '@react-three/fiber', '@react-three/drei'] }, webpack: (config) => { config.optimization.usedExports = true return config } } module.exports = nextConfig

🎯 まとめ

この記事では、Next.jsとReact Three Fiberを使用して、美しくインタラクティブな銀河系3Dビジュアルを実装する方法を詳しく解説しました。

学んだポイント:

  • パーティクルシステム: 大量の点を効率的に描画
  • 数学的アプローチ: 螺旋構造の実装
  • パフォーマンス最適化: モバイル対応とWebGL チェック
  • ユーザビリティ: リアルタイムパラメータ調整
  • Next.js 統合: SSR対応と動的インポート

次のステップ:

  1. カスタムシェーダー: より高度な視覚効果
  2. 物理演算: 重力や衝突判定の追加
  3. VR/AR対応: WebXR での没入体験
  4. データ可視化: 実際のデータとの連携

このプロジェクトを基に、さらに創造性豊かな3Dビジュアルを作成してみてください!


📚 参考リソース

Happy Coding! 🚀

コメントを投稿

メールアドレスは公開されません

最大1000文字まで。マークダウン記法(**太字**、*斜体*、`コード`など)が使用できます

投稿されたコメントは管理者による承認後に表示される場合があります。 不適切な内容は削除される可能性があります。

コメント (0件)

関連記事