1
0
Files
vrm/src/AtmosphereScene.jsx
2026-03-07 23:16:35 +09:00

148 lines
4.9 KiB
JavaScript

import React, { useEffect, useRef, useState } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { PerspectiveCamera } from '@react-three/drei';
import { EffectComposer, ToneMapping } from '@react-three/postprocessing';
import { ToneMappingMode } from 'postprocessing';
import * as THREE from 'three';
import { AerialPerspective, Atmosphere } from '@takram/three-atmosphere/r3f';
import { Clouds, CloudLayer } from '@takram/three-clouds/r3f';
import { TilesPlugin, TilesRenderer } from '3d-tiles-renderer/r3f';
import {
GLTFExtensionsPlugin,
GoogleCloudAuthPlugin,
TileCompressionPlugin,
TilesFadePlugin,
UpdateOnChangePlugin,
} from '3d-tiles-renderer/plugins';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { FollowCamera } from './controls/CameraRig';
import { worldState } from './worldState';
const TIME_SCALE = 100;
const INITIAL_DATE = new Date('2024-06-21T12:00:00');
const WEATHER_INTERVAL = 5 * 60 * 1000;
const WEATHER_PRESETS = [
{
name: 'Clear',
coverage: 0.05,
layers: [
{ channel: 'r', altitude: 2000, height: 500, densityScale: 0.05 },
{ channel: 'b', altitude: 7500, height: 500, densityScale: 0.05 },
]
},
{
name: 'Sunny',
coverage: 0.2,
layers: [
{ channel: 'r', altitude: 1500, height: 500, densityScale: 0.3 },
{ channel: 'b', altitude: 7500, height: 500, densityScale: 0.15 },
]
},
{
name: 'Cloudy',
coverage: 0.4,
layers: [
{ channel: 'r', altitude: 1500, height: 500, densityScale: 0.4 },
{ channel: 'g', altitude: 2000, height: 800, densityScale: 0.3 },
]
}
];
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
function GoogleMaps3DTiles() {
const apiKey = import.meta.env.VITE_GOOGLE_MAP_API_KEY;
if (!apiKey) return null;
return (
<TilesRenderer url={`https://tile.googleapis.com/v1/3dtiles/root.json?key=${apiKey}`}>
<TilesPlugin plugin={GoogleCloudAuthPlugin} args={{ apiToken: apiKey }} />
<TilesPlugin plugin={GLTFExtensionsPlugin} dracoLoader={dracoLoader} />
<TilesPlugin plugin={TileCompressionPlugin} />
<TilesPlugin plugin={UpdateOnChangePlugin} />
<TilesPlugin plugin={TilesFadePlugin} />
</TilesRenderer>
);
}
export default function AtmosphereScene({ timeScale: timeScaleProp, overrideDate, overrideTimeScale }) {
const { gl } = useThree();
const sunRef = useRef();
const atmosphereRef = useRef();
const dateRef = useRef(new Date(INITIAL_DATE));
const savedDateRef = useRef(null);
const timeScaleRef = useRef(timeScaleProp ?? TIME_SCALE);
// Override date for burst effect
const hasBurstRef = useRef(false);
useEffect(() => {
if (overrideDate) {
hasBurstRef.current = true;
dateRef.current = new Date(overrideDate);
} else if (hasBurstRef.current) {
hasBurstRef.current = false;
// Burst ended: set to sunrise
dateRef.current = new Date('2024-06-22T10:00:00');
}
}, [overrideDate]);
timeScaleRef.current = overrideTimeScale ?? timeScaleProp ?? TIME_SCALE;
const [weather, setWeather] = useState(WEATHER_PRESETS[1]);
useEffect(() => {
gl.toneMapping = THREE.NoToneMapping;
gl.toneMappingExposure = 10.0;
}, [gl]);
useEffect(() => {
const interval = setInterval(() => {
setWeather(prev => {
const others = WEATHER_PRESETS.filter(w => w.name !== prev.name);
return others[Math.floor(Math.random() * others.length)];
});
}, WEATHER_INTERVAL);
return () => clearInterval(interval);
}, []);
useFrame((_, delta) => {
const currentDate = dateRef.current;
currentDate.setTime(currentDate.getTime() + delta * timeScaleRef.current * 1000);
worldState.currentHour = currentDate.getHours() + currentDate.getMinutes() / 60;
if (atmosphereRef.current) {
atmosphereRef.current.updateByDate(currentDate);
const sunDirection = atmosphereRef.current.sunDirection;
if (sunRef.current && sunDirection) {
sunRef.current.position.copy(sunDirection);
sunRef.current.intensity = sunDirection.y < -0.1 ? 0.1 : 3.0;
}
}
});
return (
<>
<PerspectiveCamera makeDefault near={10} far={10000000} fov={45} />
<FollowCamera />
<directionalLight ref={sunRef} position={[0, 1, 0]} intensity={3.0} castShadow />
<Atmosphere ref={atmosphereRef}>
<GoogleMaps3DTiles />
<EffectComposer multisampling={0} disableNormalPass={false}>
<Clouds disableDefaultLayers coverage={weather.coverage}>
{weather.layers.map((layer, i) => (
<CloudLayer key={i} channel={layer.channel} altitude={layer.altitude}
height={layer.height} densityScale={layer.densityScale} shapeAmount={0.5} />
))}
</Clouds>
<AerialPerspective sky />
<ToneMapping mode={ToneMappingMode.AGX} />
</EffectComposer>
</Atmosphere>
</>
);
}