18.9 C
New York
Thursday, September 18, 2025

Creating an Immersive 3D Climate Visualization with React Three Fiber



I’ve at all times been concerned with knowledge visualization utilizing Three.js / R3F, and I believed a climate net app can be the right place to begin. One among my favourite open-source libraries, @react-three/drei, already has a bunch of nice instruments like clouds, sky, and stars that match completely into visualizing the climate in 3D.

This tutorial explores rework API knowledge right into a 3D expertise, the place we add a bit aptitude and enjoyable to climate visualization.

The Expertise Stack

Our climate world is constructed on a basis of a few of my favourite applied sciences:

Climate Parts

The guts of our visualization lies in conditionally displaying a sensible solar, moon, and/or clouds based mostly on the climate
outcomes out of your metropolis or a metropolis you seek for, particles that simulate rain or snow, day/evening logic, and a few enjoyable
lighting results throughout a thunderstorm. We’ll begin by constructing these climate parts after which transfer on to displaying
them based mostly on the outcomes of the WeatherAPI name.

Solar + Moon Implementation

Let’s begin easy: we’ll create a solar and moon part that’s only a sphere with a sensible texture wrapped
round it. We’ll additionally give it a bit rotation and a few lighting.

// Solar.js and Moon.js Part, a texture wrapped sphere
import React, { useRef } from 'react';
import { useFrame, useLoader } from '@react-three/fiber';
import { Sphere } from '@react-three/drei';
import * as THREE from 'three';

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

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

  return (
    <group place={[0, 4.5, 0]}>
      <Sphere ref={sunRef} args={[2, 32, 32]} materials={sunMaterial} />
      
      {/* Solar lighting */}
      <pointLight place={[0, 0, 0]} depth={2.5} shade="#FFD700" distance={25} />
    </group>
  );
};

export default Solar;

I grabbed the CC0 texture from right here. The moon part is actually the identical; I used this picture. The pointLight depth is low as a result of most of our lighting will come from the sky.

Rain: Instanced Cylinders

Subsequent, let’s create a rain particle impact. To maintain issues performant, we’re going to make use of instancedMesh as a substitute of making a separate mesh part for every rain particle. We’ll render a single geometry (<cylinderGeometry>) a number of instances with completely different transformations (place, rotation, scale). Additionally, as a substitute of making a brand new THREE.Object3D for every particle in each body, we’ll reuse a single dummy object. This protects reminiscence and prevents the overhead of making and garbage-collecting numerous short-term objects throughout the animation loop. We’ll additionally use the useMemo hook to create and initialize the particles array solely as soon as when the part mounts.

// Rain.js - instanced rendering
const Rain = ({ rely = 1000 }) => {
  const meshRef = useRef();
  const dummy = useMemo(() => new THREE.Object3D(), []);

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

  useFrame(() => {
    particles.forEach((particle, i) => {
      particle.y -= particle.velocity;
      if (particle.y < -1) {
        particle.y = 20; // Reset to high
      }

      dummy.place.set(particle.x, particle.y, particle.z);
      dummy.updateMatrix();
      meshRef.present.setMatrixAt(i, dummy.matrix);
    });
    meshRef.present.instanceMatrix.needsUpdate = true;
  });

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

When a particle reaches a unfavourable Y-axis degree, it’s instantly recycled to the highest of the scene with a brand new random horizontal place, creating the phantasm of steady rainfall with out consistently creating new objects.

Snow: Physics-Primarily based Tumbling

We’ll use the identical primary template for the snow impact, however as a substitute of the particles falling straight down, we’ll give them some drift.

// Snow.js - Reasonable drift and tumbling with time-based rotation
useFrame((state) => {
  particles.forEach((particle, i) => {
    particle.y -= particle.velocity;
    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.place.set(particle.x, particle.y, particle.z);
    // Time-based tumbling rotation for pure snowflake motion
    dummy.rotation.x = state.clock.elapsedTime * 2;
    dummy.rotation.y = state.clock.elapsedTime * 3;
    dummy.updateMatrix();
    meshRef.present.setMatrixAt(i, dummy.matrix);
  });
  meshRef.present.instanceMatrix.needsUpdate = true;
});

The horizontal drift makes use of Math.sin(state.clock.elapsedTime + i), the place state.clock.elapsedTime offers a repeatedly rising time worth and i offsets every particle’s timing. This creates a pure swaying movement wherein every snowflake follows its personal path. The rotation updates apply small increments to each the X and Y axes, creating the tumbling impact.

Storm System: Multi-Part Climate Occasions

When a storm rolls in, I needed to simulate darkish, brooding clouds and flashes of lightning. This impact requires combining a number of climate results concurrently. We’ll import our rain part, add some clouds, and implement a lightning impact with a pointLight that simulates flashes of lightning coming from contained in the clouds.

// Storm.js
const Storm = () => {
  const cloudsRef = useRef();
  const lightningLightRef = useRef();
  const lightningActive = useRef(false);

  useFrame((state) => {
    // Lightning flash with ambient gentle
    if (Math.random() < 0.003 && !lightningActive.present) {
      lightningActive.present = true;
      
      if (lightningLightRef.present) {
        // Random X place for every flash
        const randomX = (Math.random() - 0.5) * 10; // Vary: -5 to five
        lightningLightRef.present.place.x = randomX;
        
        // Single shiny flash
        lightningLightRef.present.depth = 90;
        
        setTimeout(() => {
          if (lightningLightRef.present) lightningLightRef.present.depth = 0;
          lightningActive.present = false;
        }, 400);
      }
    }
  });

 return (
    <group>
      <group ref={cloudsRef}>
        <DreiClouds materials={THREE.MeshLambertMaterial}>
          <Cloud
            segments={60}
            bounds={[12, 3, 3]}
            quantity={10}
            shade="#8A8A8A"
            fade={100}
            velocity={0.2}
            opacity={0.8}
            place={[-3, 4, -2]}
          />
        {/* Further cloud configurations... */}
      </DreiClouds>
      
      {/* Heavy rain - 1500 particles */}
      <Rain rely={1500} />
      
      <pointLight 
        ref={lightningLightRef}
        place={[0, 6, -5.5]}
        depth={0}
        shade="#e6d8b3"
        distance={30}
        decay={0.8}
        castShadow
      />
    </group>
  );
};

The lightning system makes use of a easy ref-based cooldown mechanism to stop fixed flashing. When lightning triggers, it creates a single shiny flash with random positioning. The system makes use of setTimeout to reset the sunshine depth after 400ms, creating a sensible lightning impact with out advanced multi-stage sequences.

Clouds: Drei Cloud

For climate varieties like cloudy, partly cloudy, overcast, foggy, wet, snowy, and misty, we’ll pull in our clouds part. I needed the storm part to have its personal clouds as a result of storms ought to have darker clouds than the circumstances above. The clouds part will merely show Drei clouds, and we’ll pull all of it along with the solar or moon part within the subsequent part.

const Clouds = ({ depth = 0.7, velocity = 0.1 }) => {
  // Decide cloud colours based mostly on climate situation
  const getCloudColors = () => {
      return {
        major: '#FFFFFF',
        secondary: '#F8F8F8',
        tertiary: '#F0F0F0',
        gentle: '#FAFAFA',
        depth: depth
      };
  };

  const colours = getCloudColors();
  return (
    <group>
      <DreiClouds materials={THREE.MeshLambertMaterial}>
        {/* Giant fluffy cloud cluster */}
        <Cloud
          segments={80}
          bounds={[12, 4, 4]}
          quantity={15}
          shade={colours.major}
          fade={50}
          velocity={velocity}
          opacity={colours.depth}
          place={[-5, 4, -2]}
        />
        {/* Further clouds... */}
      </DreiClouds>
    </group>
  );
};

API-Pushed Logic: Placing It All Collectively

Now that we’ve constructed our climate parts, we’d like a system to resolve which of them to show based mostly on actual climate knowledge. The WeatherAPI.com service offers detailed present circumstances that we’ll rework into our 3D scene parameters. The API provides us situation textual content like “Partly cloudy,” “Thunderstorm,” or “Gentle snow,” however we have to convert these into our part varieties.

// weatherService.js - Fetching actual climate knowledge
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 }
);

The API request consists of time zone info so we will precisely decide day or evening for our Solar/Moon system. The days=3 parameter grabs forecast knowledge for our portal function, whereas aqi=no&alerts=no retains the payload lean by excluding knowledge we don’t want.

Changing API Circumstances to Part Sorts

The guts of our system is a straightforward parsing perform that maps lots of of attainable climate descriptions to our manageable set of visible parts:

// weatherService.js - Changing climate textual content to renderable varieties
export const getWeatherConditionType = (situation) => {
  const conditionLower = situation.toLowerCase();

  if (conditionLower.consists of('sunny') || conditionLower.consists of('clear')) {
    return 'sunny';
  }
  if (conditionLower.consists of('thunder') || conditionLower.consists of('storm')) {
    return 'stormy';
  }
  if (conditionLower.consists of('cloud') || conditionLower.consists of('overcast')) {
    return 'cloudy';
  }
  if (conditionLower.consists of('rain') || conditionLower.consists of('drizzle')) {
    return 'wet';
  }
  if (conditionLower.consists of('snow') || conditionLower.consists of('blizzard')) {
    return 'snowy';
  }
  // ... further fog and mist circumstances
  return 'cloudy';
};

This string-matching strategy handles edge circumstances gracefully—whether or not the API returns “Gentle rain,” “Heavy rain,” or “Patchy gentle drizzle,” all of them map to our wet kind and set off the suitable 3D results. This manner, we will reuse our most important parts without having a separate part for every climate situation.

Conditional Part Rendering

The magic occurs in our WeatherVisualization part, the place the parsed climate kind determines precisely which 3D parts to render:

// WeatherVisualization.js - Bringing climate knowledge to life
const renderWeatherEffect = () => {
  if (weatherType === 'sunny') {
    if (partlyCloudy) {
      return (
        <>
          {isNight ? <Moon /> : <Solar />}
          <Clouds depth={0.5} velocity={0.1} />
        </>
      );
    }
    return isNight ? <Moon /> : <Solar />;
  } else if (weatherType === 'wet') {
    return (
      <>
        <Clouds depth={0.8} velocity={0.15} />
        <Rain rely={800} />
      </>
    );
  } else if (weatherType === 'stormy') {
    return <Storm />; // Consists of its personal clouds, rain, and lightning
  }
  // ... further climate varieties
};

This conditional system ensures we solely load the particle methods we really want. A sunny day renders simply our Solar part, whereas a storm masses our full Storm system with heavy rain, darkish clouds, and lightning results. Every climate kind will get its personal mixture of the parts we constructed earlier, creating distinct visible experiences that match the actual climate circumstances.

Dynamic Time-of-Day System

Climate isn’t nearly circumstances—it’s additionally about timing. Our climate parts must know whether or not to point out the solar or moon, and we have to configure Drei’s Sky part to render the suitable atmospheric colours for the present time of day. Thankfully, our WeatherAPI response already consists of the native time for any location, so we will extract that to drive our day/evening logic.

The API offers native time in a easy format that we will parse to find out the present interval:

// Scene3D.js - Parsing time from climate API knowledge
const getTimeOfDay = () =>  currentHour <= 6) return 'evening';
  if (currentHour >= 6 && currentHour < 8) return 'daybreak';
  if (currentHour >= 17 && currentHour < 19) return 'nightfall';
  return 'day';
;

This provides us 4 distinct time durations, every with completely different lighting and sky configurations. Now we will use these durations to configure Drei’s Sky part, which handles atmospheric scattering and generates life like sky colours.

Dynamic Sky Configuration

Drei’s Sky part is incredible as a result of it simulates precise atmospheric physics—we simply want to regulate atmospheric parameters for every time interval:

// Scene3D.js - Time-responsive Sky configuration
{timeOfDay !== 'evening' && (
  <Sky
    sunPosition={(() => {
      if (timeOfDay === 'daybreak') {
        return [100, -5, 100]; // Solar under horizon for darker daybreak colours
      } else if (timeOfDay === 'nightfall') {
        return [-100, -5, 100]; // Solar under horizon for sundown colours
      } else { // day
        return [100, 20, 100]; // Excessive solar place for shiny daylight
      }
    })()}
    inclination={(() => {
      if (timeOfDay === 'daybreak' || timeOfDay === 'nightfall') {
        return 0.6; // Medium inclination for transitional durations
      } else { // day
        return 0.9; // Excessive inclination for clear daytime sky
      }
    })()}
    turbidity={(() => {
      if (timeOfDay === 'daybreak' || timeOfDay === 'nightfall') {
        return 8; // Greater turbidity creates heat dawn/sundown colours
      } else { // day
        return 2; // Decrease turbidity for clear blue sky
      }
    })()}
  />
)}

The magic occurs within the positioning. Throughout daybreak and nightfall, we place the solar slightly below the horizon (-5 Y place) so Drei’s Sky part generates these heat orange and pink colours we affiliate with dawn and sundown. The turbidity parameter controls atmospheric scattering, with increased values creating extra dramatic shade results throughout transitional durations.

Nighttime: Easy Black Background + Stars

For nighttime, I made a deliberate option to skip Drei’s Sky part solely and use a easy black background as a substitute. The Sky part could be computationally costly, and for nighttime scenes, a pure black backdrop truly seems higher and performs considerably quicker. We complement this with Drei’s Stars part for that genuine nighttime ambiance:

// Scene3D.js - Environment friendly nighttime rendering
{!portalMode && isNight && <SceneBackground backgroundColor={'#000000'} />}

{/* Stars create the nighttime ambiance */}
{isNight && (
  <Stars
    radius={100}
    depth={50}
    rely={5000}
    issue={4}
    saturation={0}
    fade
    velocity={1}
  />
)}

Drei’s Stars part creates 5,000 particular person stars scattered throughout a 100-unit sphere with life like depth variation. The saturation={0} retains them correctly desaturated for genuine nighttime visibility, whereas the light velocity={1} creates refined motion that simulates the pure movement of celestial our bodies. Stars solely seem throughout nighttime hours (7 PM to six AM) and robotically disappear at daybreak, making a clean transition again to Drei’s daytime Sky part.

This strategy provides us 4 distinct atmospheric moods—shiny daylight, heat daybreak colours, golden nightfall tones, and star-filled nights—all pushed robotically by the actual native time from our climate knowledge.

Forecast Portals: Home windows Into Tomorrow’s Climate

Like every good climate app, we don’t need to simply present present circumstances but in addition what’s coming subsequent. Our API returns a three-day forecast that we rework into three interactive portals hovering within the 3D scene, each displaying a preview of that day’s climate circumstances. Click on on a portal and also you’re transported into that day’s atmospheric surroundings.

Constructing Portals with MeshPortalMaterial

The portals use Drei’s MeshPortalMaterial, which renders an entire 3D scene to a texture that will get mapped onto a airplane. Every portal turns into a window into its personal climate world:

// ForecastPortals.js - Creating interactive climate portals
const ForecastPortal = ({ place, dayData, index, onEnter }) => {
  const materialRef = useRef();

  // Remodel forecast API knowledge into our climate part format
  const portalWeatherData = useMemo(() => ({
    present: {
      temp_f: dayData.day.maxtemp_f,
      situation: dayData.day.situation,
      is_day: 1, // Pressure daytime for constant portal lighting
      humidity: dayData.day.avghumidity,
      wind_mph: dayData.day.maxwind_mph,
    },
    location: {
      localtime: dayData.date + 'T12:00' // Set to midday for optimum lighting
    }
  }), [dayData]);

  return (
    <group place={place}>
      <mesh onClick={onEnter}>
        <roundedPlaneGeometry args={[2, 2.5, 0.15]} />
        <MeshPortalMaterial
          ref={materialRef}
          blur={0}
          decision={256}
          worldUnits={false}
        >
          {/* Every portal renders an entire climate scene */}
          <shade connect="background" args={['#87CEEB']} />
          <ambientLight depth={0.4} />
          <directionalLight place={[10, 10, 5]} depth={1} />
          <WeatherVisualization
            weatherData={portalWeatherData}
            isLoading={false}
            portalMode={true}
          />
        </MeshPortalMaterial>
      </mesh>

      {/* Climate information overlay */}
      <Textual content place={[-0.8, 1.0, 0.1]} fontSize={0.18} shade="#FFFFFF">
        {formatDay(dayData.date, index)}
      </Textual content>
      <Textual content place={[0.8, 1.0, 0.1]} fontSize={0.15} shade="#FFFFFF">
        {Math.spherical(dayData.day.maxtemp_f)}° / {Math.spherical(dayData.day.mintemp_f)}°
      </Textual content>
      <Textual content place={[-0.8, -1.0, 0.1]} fontSize={0.13} shade="#FFFFFF">
        {dayData.day.situation.textual content}
      </Textual content>
    </group>
  );
};

The roundedPlaneGeometry from the maath library provides our portals these clean, natural edges as a substitute of sharp rectangles. The [2, 2.5, 0.15] parameters create a 2×2.5 unit portal with 0.15 radius corners, offering sufficient rounding to look visually interesting.

Interactive States and Animations

Portals reply to consumer interplay with clean state transitions. The system tracks two major states: inactive and fullscreen:

// ForecastPortals.js - State administration and mix animations
const ForecastPortal = ({ place, dayData, isActive, isFullscreen, onEnter }) => {
  const materialRef = useRef();

  useFrame(() => {
    if (materialRef.present)  0,
        targetBlend,
        0.1
      );
    
  });

  // Portal content material and UI components hidden in fullscreen mode
  return (
    <group place={place}>
      <mesh onClick={onEnter}>
        <roundedPlaneGeometry args={[2, 2.5, 0.15]} />
        <MeshPortalMaterial ref={materialRef}>
          <PortalScene />
        </MeshPortalMaterial>
      </mesh>

      {!isFullscreen && (
        <>
          {/* Temperature and situation textual content solely present in preview mode */}
          <Textual content place={[-0.8, 1.0, 0.1]} fontSize={0.18} shade="#FFFFFF">
            {formatDay(dayData.date, index)}
          </Textual content>
        </>
      )}
    </group>
  );
};

The mix property controls how a lot the portal takes over your view. At 0 (inactive), you see the portal as a framed window into the climate scene. At 1 (fullscreen), you’re utterly transported inside that day’s climate surroundings. The THREE.MathUtils.lerp perform creates clean transitions between these two states when clicking out and in of portals.

Fullscreen Portal Expertise

Once you click on a portal, it fills your whole view with that day’s climate. As a substitute of tomorrow’s climate by way of a window, you’re standing inside it:

// Scene3D.js - Fullscreen portal dealing with
const handlePortalStateChange = (isPortalActive, dayData) => {
  setPortalMode(isPortalActive);
  if (isPortalActive && dayData) {
    // Create immersive climate surroundings for the chosen day
    const portalData = {
      present: {
        temp_f: dayData.day.maxtemp_f,
        situation: dayData.day.situation,
        is_day: 1,
        humidity: dayData.day.avghumidity,
      },
      location: { localtime: dayData.date + 'T12:00' }
    };
    setPortalWeatherData(portalData);
  }
};

In fullscreen mode, the portal climate knowledge drives your entire scene: the Sky part, lighting, and all climate results now symbolize that forecasted day. You possibly can orbit round inside tomorrow’s storm or bask within the light daylight of the day after. Once you exit (click on exterior the portal), the system easily transitions again to the present climate circumstances.

The important thing perception is that every portal runs our identical WeatherVisualization part however with forecast knowledge as a substitute of present circumstances. The portalMode={true} prop optimizes the parts for smaller render targets—fewer particles, less complicated clouds, however the identical conditional logic we constructed earlier.

Now that we’ve launched portals, we have to replace our climate parts to help this optimization. Going again to our conditional rendering examples, we add the portalMode prop:

// WeatherVisualization.js - Up to date with portal help
if (weatherType === 'wet') {
  return (
    <>
      <Clouds depth={0.8} velocity={0.15} portalMode={portalMode} />
      <Rain rely={portalMode ? 100 : 800} />
    </>
  );
} else if (weatherType === 'snowy') {
  return (
    <>
      <Clouds depth={0.6} velocity={0.05} portalMode={portalMode} />
      <Snow rely={portalMode ? 50 : 400} />
    </>
  );
}

And our Clouds part is up to date to render fewer, less complicated clouds in portal mode:

// Clouds.js - Portal optimization
const Clouds = ({ depth = 0.7, velocity = 0.1, portalMode = false }) => {
  if (portalMode) {
    return (
      <DreiClouds materials={THREE.MeshLambertMaterial}>
        {/* Solely 2 centered clouds for portal preview */}
        <Cloud segments={40} bounds={[8, 3, 3]} quantity={8} place={[0, 4, -2]} />
        <Cloud segments={35} bounds={[6, 2.5, 2.5]} quantity={6} place={[2, 3, -3]} />
      </DreiClouds>
    );
  }
  // Full cloud system for most important scene (6+ detailed clouds)
  return <group>{/* ... full cloud configuration ... */}</group>;
};

This dramatically reduces each particle counts (87.5% fewer rain particles) and cloud complexity (a 67% discount from 6 detailed clouds to 2 centered clouds), making certain clean efficiency when a number of portals present climate results concurrently.

Integration with Scene3D

The portals are positioned and managed in our most important Scene3D part, the place they complement the present climate visualization:

// Scene3D.js - Portal integration
<>
  {/* Present climate in the primary scene */}
  <WeatherVisualization
    weatherData={weatherData}
    isLoading={isLoading}
  />

  {/* Three-day forecast portals */}
  <ForecastPortals
    weatherData={weatherData}
    isLoading={isLoading}
    onPortalStateChange={handlePortalStateChange}
  />
</>

Once you click on a portal, your entire scene transitions to fullscreen mode, displaying that day’s climate in full element. The portal system tracks lively states and handles clean transitions between preview and immersive modes, making a seamless solution to discover future climate circumstances alongside the present atmospheric surroundings.

The portals rework static forecast numbers into explorable 3D environments. As a substitute of studying “Tomorrow: 75°, Partly Cloudy,” you see and really feel the light drift of cumulus clouds with heat daylight filtering by way of.

Including Cinematic Lens Flares

Our Solar part seems nice, however to essentially make it really feel cinematic, I needed to implement a refined lens flare impact. For this, I’m utilizing the R3F-Final-Lens-Flare library (shoutout to Anderson Mancini), which I put in manually by following the repository’s directions. Whereas lens flares usually work finest with distant solar objects moderately than our close-up strategy, I nonetheless assume it provides a pleasant cinematic contact to the scene.

The lens flare system must be good about when to seem. Identical to our climate parts, it ought to solely present when it makes meteorological sense:

// Scene3D.js - Conditional lens flare rendering
const PostProcessingEffects = ({ showLensFlare }) => {
  if (!showLensFlare) return null;

  return (
    <EffectComposer>
      <UltimateLensFlare
        place={[0, 5, 0]} // Positioned close to our Solar part at [0, 4.5, 0]
        opacity={1.00}
        glareSize={1.68}
        starPoints={2}
        animated={false}
        flareShape={0.81}
        flareSize={1.68}
        secondaryGhosts={true}
        ghostScale={0.03}
        aditionalStreaks={true}
        haloScale={3.88}
      />
      <Bloom depth={0.3} threshold={0.9} />
    </EffectComposer>
  );
};

The important thing parameters create a sensible lens flare impact: glareSize and flareSize each at 1.68 give outstanding however not overwhelming flares, whereas ghostScale={0.03} provides refined lens reflection artifacts. The haloScale={3.88} creates that enormous atmospheric glow across the solar.

The lens flare connects to our climate system by way of a visibility perform that determines when the solar must be seen:

// weatherService.js - When ought to we present lens flares?
export const shouldShowSun = (weatherData) => {
  if (!weatherData?.present?.situation) return true;
  const situation = weatherData.present.situation.textual content.toLowerCase();

  // Cover lens flare when climate obscures the solar
  if (situation.consists of('overcast') ||
      situation.consists of('rain') ||
      situation.consists of('storm') ||
      situation.consists of('snow')) {
    return false;
  }

  return true; // Present for clear, sunny, partly cloudy circumstances
};

// Scene3D.js - Combining climate and time circumstances
const showLensFlare = useMemo(() => , [isNight, weatherData]);

This creates life like conduct the place lens flares solely seem throughout daytime clear climate. Throughout storms, the solar (and its lens flare) is hidden by clouds, identical to in actual life.

Efficiency Optimizations

Since we’re rendering 1000’s of particles, a number of cloud methods, and interactive portals—typically concurrently—it may get costly. As talked about above, all our particle methods use instanced rendering to attract 1000’s of raindrops or snowflakes in single GPU calls. Conditional rendering ensures we solely load the climate results we really want: no rain particles throughout sunny climate, no lens flares throughout storms. Nevertheless, there’s nonetheless plenty of room for optimization. Essentially the most vital enchancment comes from our portal system’s adaptive rendering. We already mentioned reducing the variety of clouds in portals above, however when a number of forecast portals present precipitation concurrently, we dramatically cut back particle counts.

// WeatherVisualization.js - Sensible particle scaling
{weatherType === 'wet' && <Rain rely={portalMode ? 100 : 800} />}
{weatherType === 'snowy' && <Snow rely={portalMode ? 50 : 400} />}

This prevents the less-than-ideal state of affairs of rendering 4 × 800 = 3,200 rain particles when all portals present rain. As a substitute, we get 800 + (3 × 100) = 1,100 whole particles whereas sustaining the visible impact.

API Reliability and Caching

Past 3D efficiency, we’d like the app to work reliably even when the climate API is sluggish, down, or rate-limited. The system implements good caching and swish degradation to maintain the expertise clean.

Clever Caching

Reasonably than hitting the API for each request, we cache climate responses for 10 minutes:

// api/climate.js - Easy however efficient caching
const cache = new Map();
const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes

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

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

This provides customers prompt responses for not too long ago searched places and retains the app responsive throughout API slowdowns.

Fee Limiting and Fallback

When customers exceed our 15 requests per hour restrict, the system easily switches to demo knowledge as a substitute of displaying errors:

// weatherService.js - Sleek degradation
if (error.response?.standing === 429) {
  console.log('Too many requests');
  return getDemoWeatherData(location);
}

The demo knowledge consists of time-aware day/evening detection, so even the fallback expertise reveals correct lighting and sky colours based mostly on the consumer’s native time.

Future Enhancements

There’s loads of room to develop this climate world. Including correct moon phases would carry one other layer of realism to nighttime scenes—proper now our moon is perpetually full. Wind results might animate vegetation or create drifting fog patterns, utilizing the wind velocity knowledge we’re already fetching however not but visualizing. Efficiency-wise, the present optimizations deal with most eventualities properly, however there’s nonetheless room for enchancment, particularly when all forecast portals present precipitation concurrently.

Conclusion

Constructing this 3D climate visualization mixed React Three Fiber with real-time meteorological knowledge to create one thing past a conventional climate app. By leveraging Drei’s ready-made parts alongside customized particle methods, we’ve reworked API responses into explorable atmospheric environments.

The technical basis combines a number of key approaches:

  • Instanced rendering for particle methods that keep 60fps whereas simulating 1000’s of raindrops
  • Conditional part loading that solely renders the climate results at present wanted
  • Portal-based scene composition utilizing MeshPortalMaterial for forecast previews
  • Time-aware atmospheric rendering with Drei’s Sky part responding to native dawn and sundown
  • Sensible caching and fallback methods that preserve the expertise responsive throughout API limitations

This was one thing I at all times needed to construct, and I had a ton of enjoyable bringing it to life!



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles