使用React Three Fiber创建沉浸式3D天气可视化应用

本文详细介绍了如何使用React Three Fiber和Three.js构建交互式3D天气可视化应用,包含太阳、月亮、雨雪粒子系统、风暴特效的实现,以及基于WeatherAPI的动态天气数据集成和性能优化技巧。

使用React Three Fiber创建沉浸式3D天气可视化应用

技术栈

我们的天气世界建立在以下核心技术之上:

  • React Three Fiber: Three.js的React渲染器
  • @react-three/drei: 提供云层、天空、星星等必备组件
  • R3F-Ultimate-Lens-Flare: Anderson Mancini开发的镜头光晕系统
  • WeatherAPI.com: 实时气象数据接口

天气组件实现

太阳和月亮组件

通过纹理贴图球体实现太阳和月亮效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Sun.js组件
import React, { useRef } from 'react';
import { useFrame, useLoader } from '@react-three/fiber';
import { Sphere } from '@react-three/drei';
import * as THREE from 'three';

const Sun = () => {
  const sunRef = useRef();
  const sunTexture = useLoader(THREE.TextureLoader, '/textures/sun_2k.jpg');
  
  useFrame((state) => {
    if (sunRef.current) {
      sunRef.current.rotation.y = state.clock.getElapsedTime() * 0.1;
    }
  });

  const sunMaterial = new THREE.MeshBasicMaterial({
    map: sunTexture,
  });

  return (
    <group position={[0, 4.5, 0]}>
      <Sphere ref={sunRef} args={[2, 32, 32]} material={sunMaterial} />
      <pointLight position={[0, 0, 0]} intensity={2.5} color="#FFD700" distance={25} />
    </group>
  );
};

降雨粒子系统

使用实例化网格实现高性能降雨效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Rain.js - 实例化渲染
const Rain = ({ count = 1000 }) => {
  const meshRef = useRef();
  const dummy = useMemo(() => new THREE.Object3D(), []);

  const particles = useMemo(() => {
    const temp = [];
    for (let i = 0; i < count; i++) {
      temp.push({
        x: (Math.random() - 0.5) * 20,
        y: Math.random() * 20 + 10,
        z: (Math.random() - 0.5) * 20,
        speed: Math.random() * 0.1 + 0.05,
      });
    }
    return temp;
  }, [count]);

  useFrame(() => {
    particles.forEach((particle, i) => {
      particle.y -= particle.speed;
      if (particle.y < -1) {
        particle.y = 20; // 重置到顶部
      }
      dummy.position.set(particle.x, particle.y, particle.z);
      dummy.updateMatrix();
      meshRef.current.setMatrixAt(i, dummy.matrix);
    });
    meshRef.current.instanceMatrix.needsUpdate = true;
  });

  return (
    <instancedMesh ref={meshRef} args={[null, null, count]}>
      <cylinderGeometry args={[0.01, 0.01, 0.5, 8]} />
      <meshBasicMaterial color="#87CEEB" transparent opacity={0.6} />
    </instancedMesh>
  );
};

雪景效果

基于物理的飘雪效果,包含漂移和旋转:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Snow.js - 真实漂移和旋转
useFrame((state) => {
  particles.forEach((particle, i) => {
    particle.y -= particle.speed;
    particle.x += Math.sin(state.clock.elapsedTime + i) * particle.drift;
    
    if (particle.y < -1) {
      particle.y = 20;
      particle.x = (Math.random() - 0.5) * 20;
    }

    dummy.position.set(particle.x, particle.y, particle.z);
    // 基于时间的旋转实现自然雪花运动
    dummy.rotation.x = state.clock.elapsedTime * 2;
    dummy.rotation.y = state.clock.elapsedTime * 3;
    dummy.updateMatrix();
    meshRef.current.setMatrixAt(i, dummy.matrix);
  });
  meshRef.current.instanceMatrix.needsUpdate = true;
});

风暴系统

结合多种天气效果的复合系统:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Storm.js
const Storm = () => {
  const cloudsRef = useRef();
  const lightningLightRef = useRef();
  const lightningActive = useRef(false);

  useFrame((state) => {
    // 闪电效果
    if (Math.random() < 0.003 && !lightningActive.current) {
      lightningActive.current = true;
      
      if (lightningLightRef.current) {
        const randomX = (Math.random() - 0.5) * 10;
        lightningLightRef.current.position.x = randomX;
        lightningLightRef.current.intensity = 90;
        
        setTimeout(() => {
          if (lightningLightRef.current) lightningLightRef.current.intensity = 0;
          lightningActive.current = false;
        }, 400);
      }
    }
  });

  return (
    <group>
      <group ref={cloudsRef}>
        <DreiClouds material={THREE.MeshLambertMaterial}>
          <Cloud
            segments={60}
            bounds={[12, 3, 3]}
            volume={10}
            color="#8A8A8A"
            fade={100}
            speed={0.2}
            opacity={0.8}
            position={[-3, 4, -2]}
          />
        </DreiClouds>
      
      <Rain count={1500} />
      
      <pointLight 
        ref={lightningLightRef}
        position={[0, 6, -5.5]}
        intensity={0}
        color="#e6d8b3"
        distance={30}
        decay={0.8}
        castShadow
      />
    </group>
  );
};

API驱动逻辑

天气数据获取

1
2
3
4
5
// weatherService.js - 获取实时天气数据
const response = await axios.get(
  `${WEATHER_API_BASE}/forecast.json?key=${API_KEY}&q=${location}&days=3&aqi=no&alerts=no&tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`,
  { timeout: 10000 }
);

天气条件映射

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 将API天气文本转换为可渲染类型
export const getWeatherConditionType = (condition) => {
  const conditionLower = condition.toLowerCase();

  if (conditionLower.includes('sunny') || conditionLower.includes('clear')) {
    return 'sunny';
  }
  if (conditionLower.includes('thunder') || conditionLower.includes('storm')) {
    return 'stormy';
  }
  // ... 其他条件映射
  return 'cloudy';
};

条件渲染系统

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// WeatherVisualization.js - 根据天气数据渲染对应组件
const renderWeatherEffect = () => {
  if (weatherType === 'sunny') {
    if (partlyCloudy) {
      return (
        <>
          {isNight ? <Moon /> : <Sun />}
          <Clouds intensity={0.5} speed={0.1} />
        </>
      );
    }
    return isNight ? <Moon /> : <Sun />;
  } else if (weatherType === 'rainy') {
    return (
      <>
        <Clouds intensity={0.8} speed={0.15} />
        <Rain count={800} />
      </>
    );
  } else if (weatherType === 'stormy') {
    return <Storm />;
  }
};

动态时间系统

时间检测

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Scene3D.js - 从天气API数据解析时间
const getTimeOfDay = () => {
  if (!weatherData?.location?.localtime) return 'day';
  const localTime = weatherData.location.localtime;
  const currentHour = new Date(localTime).getHours();

  if (currentHour >= 19 || currentHour <= 6) return 'night';
  if (currentHour >= 6 && currentHour < 8) return 'dawn';
  if (currentHour >= 17 && currentHour < 19) return 'dusk';
  return 'day';
};

天空配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Scene3D.js - 时间响应式天空配置
{timeOfDay !== 'night' && (
  <Sky
    sunPosition={(() => {
      if (timeOfDay === 'dawn') return [100, -5, 100];
      else if (timeOfDay === 'dusk') return [-100, -5, 100];
      else return [100, 20, 100];
    })()}
    inclination={timeOfDay === 'dawn' || timeOfDay === 'dusk' ? 0.6 : 0.9}
    turbidity={timeOfDay === 'dawn' || timeOfDay === 'dusk' ? 8 : 2}
  />
)}

预测门户系统

门户组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ForecastPortals.js - 创建交互式天气门户
const ForecastPortal = ({ position, dayData, index, onEnter }) => {
  const materialRef = useRef();

  const portalWeatherData = useMemo(() => ({
    current: {
      temp_f: dayData.day.maxtemp_f,
      condition: dayData.day.condition,
      is_day: 1,
      humidity: dayData.day.avghumidity,
      wind_mph: dayData.day.maxwind_mph,
    },
    location: {
      localtime: dayData.date + 'T12:00'
    }
  }), [dayData]);

  return (
    <group position={position}>
      <mesh onClick={onEnter}>
        <roundedPlaneGeometry args={[2, 2.5, 0.15]} />
        <MeshPortalMaterial
          ref={materialRef}
          blur={0}
          resolution={256}
          worldUnits={false}
        >
          <color attach="background" args={['#87CEEB']} />
          <ambientLight intensity={0.4} />
          <directionalLight position={[10, 10, 5]} intensity={1} />
          <WeatherVisualization
            weatherData={portalWeatherData}
            isLoading={false}
            portalMode={true}
          />
        </MeshPortalMaterial>
      </mesh>
    </group>
  );
};

性能优化

智能粒子缩放

1
2
3
// WeatherVisualization.js - 根据门户模式调整粒子数量
{weatherType === 'rainy' && <Rain count={portalMode ? 100 : 800} />}
{weatherType === 'snowy' && <Snow count={portalMode ? 50 : 400} />}

API缓存机制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// api/weather.js - 缓存优化
const cache = new Map();
const CACHE_DURATION = 10 * 60 * 1000; // 10分钟

const cacheKey = `weather:${location.toLowerCase()}`;
const cachedData = cache.get(cacheKey);

if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) {
  return res.json({ ...cachedData.data, cached: true });
}

结论

这个3D天气可视化项目将React Three Fiber与实时气象数据相结合,创建了超越传统天气应用的沉浸式体验。通过关键技术实现包括:

  • 实例化渲染保持60fps性能
  • 条件组件加载优化资源使用
  • 基于MeshPortalMaterial的门户场景合成
  • 时间感知的大气渲染
  • 智能缓存和降级系统

该项目展示了现代WebGL技术与实时数据可视化的完美结合。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计