3_vr.tsx (5662B)
1 "use client"; 2 3 import { Canvas, useFrame } from "@react-three/fiber"; 4 import { 5 OrbitControls, 6 Sky, 7 ContactShadows, 8 Cloud, 9 Clouds, 10 } from "@react-three/drei"; 11 import BenchScene from "./BenchScene.tsx"; 12 import { useState, useMemo, useRef, useEffect } from "react"; 13 import * as THREE from "three"; 14 import { StageProps } from "../ctrl/page.tsx"; 15 16 const AUDIO_SOURCES: Record<number, string> = { 17 1: "/audio/003-1.flac", 18 2: "/audio/003-2.flac", 19 }; 20 21 const useSequentialAudio = ( 22 initialState: number, 23 audioSources: Record<number, string>, 24 onComplete: () => void, 25 ) => { 26 const [sceneState, setSceneState] = useState(initialState); 27 const audioRef = useRef<HTMLAudioElement | null>(null); 28 29 const currentAudioUrl = audioSources[sceneState]; 30 31 useEffect(() => { 32 if (sceneState > Math.max(...Object.keys(audioSources).map(Number))) { 33 console.log("All audio finished. Calling onComplete."); 34 onComplete(); 35 return; 36 } 37 38 if (!currentAudioUrl) { 39 return; 40 } 41 42 console.log(`Starting setup for state ${sceneState}: ${currentAudioUrl}`); 43 44 const existingAudio = audioRef.current; 45 if (existingAudio) { 46 existingAudio.pause(); 47 } 48 49 const audio = new Audio(currentAudioUrl); 50 audioRef.current = audio; 51 52 const handleAudioEnded = () => { 53 console.log(`Audio ${sceneState} finished. Transitioning state.`); 54 // 次の状態へ遷移 (例: 1 -> 2, 2 -> 3) 55 setSceneState((prev) => prev + 1); 56 }; 57 58 audio.addEventListener("ended", handleAudioEnded); 59 60 audio.play().catch((e) => { 61 console.warn( 62 `Audio playback failed for state ${sceneState} (requires user interaction):`, 63 e, 64 ); 65 }); 66 67 return () => { 68 if (audio === audioRef.current) { 69 audio.pause(); 70 audio.removeEventListener("ended", handleAudioEnded); 71 } 72 }; 73 }, [sceneState, audioSources, onComplete]); 74 return sceneState; 75 }; 76 77 interface RandomCloudsProps { 78 count: number; 79 radius: number; 80 height: number; 81 sceneState: number; 82 } 83 84 function SceneController({ sceneState }: { sceneState: number }) { 85 const isFoggy = sceneState === 2; 86 87 const directionalLightIntensity = isFoggy ? 0.1 : 2; 88 89 const ambientLightIntensity = isFoggy ? 0.05 : 0.5; 90 91 useFrame(({ scene }) => { 92 if (isFoggy) { 93 scene.fog = new THREE.Fog(0x444444, 5, 40); // Far distanceも少し短くして密度を上げる 94 } else { 95 scene.fog = null; 96 } 97 }); 98 99 return ( 100 <> 101 <ambientLight intensity={ambientLightIntensity} /> 102 103 <directionalLight 104 position={[5, 10, 5]} 105 intensity={directionalLightIntensity} // ここで制御 106 castShadow 107 shadow-mapSize-width={2048} 108 shadow-mapSize-height={2048} 109 shadow-camera-far={50} 110 shadow-camera-left={-10} 111 shadow-camera-right={10} 112 shadow-camera-top={10} 113 shadow-camera-bottom={-10} 114 /> 115 </> 116 ); 117 } 118 119 function AnimatedSky({ sceneState }: { sceneState: number }) { 120 const [sunPosition, setSunPosition] = useState<[number, number, number]>([ 121 0, 100, 0, 122 ]); 123 124 useFrame(({ clock }) => { 125 const time = clock.getElapsedTime() * 0.1; 126 const x = Math.sin(time) * 100; 127 const z = Math.cos(time) * 100; 128 setSunPosition([x, 100, z]); 129 }); 130 131 if (sceneState === 2) { 132 return null; 133 } 134 135 return ( 136 <Sky 137 distance={450000} 138 sunPosition={new THREE.Vector3(...sunPosition)} 139 inclination={sceneState === 2 ? 0.001 : 0.6} 140 azimuth={0.25} 141 mieCoefficient={0.005} 142 mieDirectionalG={0.8} 143 rayleigh={0.5} 144 turbidity={10} 145 /> 146 ); 147 } 148 149 function RandomClouds({ 150 count, 151 radius, 152 height, 153 sceneState, 154 }: RandomCloudsProps) { 155 const cloudColor = sceneState === 2 ? "#080808" : "#fff"; 156 const baseOpacity = sceneState === 2 ? 0.9 : 0.5; 157 158 const cloudConfigs = useMemo(() => { 159 return Array.from({ length: count }).map((_, i) => { 160 const angle = Math.random() * Math.PI * 2; 161 const distance = Math.sqrt(Math.random()) * radius; 162 163 const x = Math.cos(angle) * distance; 164 const z = Math.sin(angle) * distance; 165 166 return { 167 position: [x, height + (Math.random() - 0.5) * 5, z] as [ 168 number, 169 number, 170 number, 171 ], 172 173 bounds: [ 174 Math.random() * 10 + 10, 175 Math.random() * 2 + 1, 176 Math.random() * 10 + 10, 177 ] as [number, number, number], 178 seed: Math.floor(Math.random() * 1000) + i, 179 volume: Math.random() * 20 + 5, 180 opacity: Math.random() * 0.4 + baseOpacity, 181 color: cloudColor, 182 }; 183 }); 184 }, [count, radius, height]); 185 186 return ( 187 <> 188 {cloudConfigs.map((config, index) => ( 189 <Cloud key={index} {...config} /> 190 ))} 191 </> 192 ); 193 } 194 195 export default function ThreeVR({ onComplete }: StageProps) { 196 const sceneState = useSequentialAudio(1, AUDIO_SOURCES, onComplete); 197 198 return ( 199 <Canvas 200 key="r3f-main-canvas" 201 camera={{ position: [5, 5, 5], fov: 60 }} 202 shadows 203 style={{ background: sceneState === 2 ? "#85555B" : "#d0e0ff" }} 204 > 205 <AnimatedSky sceneState={sceneState} /> 206 207 <Clouds material={THREE.MeshLambertMaterial} limit={200}> 208 <RandomClouds 209 count={100} 210 radius={80} 211 height={15} 212 sceneState={sceneState} 213 /> 214 </Clouds> 215 216 <SceneController sceneState={sceneState} /> 217 218 <BenchScene /> 219 <ContactShadows 220 position={[0, -0.5, 0]} 221 opacity={0.7} 222 scale={10} 223 blur={1} 224 far={10} 225 /> 226 <OrbitControls enableZoom={true} enablePan={true} enableRotate={true} /> 227 </Canvas> 228 ); 229 }