react-globe.gl icon indicating copy to clipboard operation
react-globe.gl copied to clipboard

Day/night cycle implementation - how to get scene from globe ref?

Open micarner opened this issue 1 year ago • 8 comments

Hey there, I have created a custom shader to blend day/night images. I have also been able to get it to rotate based on a 0 to 1 integer where it goes all the way around the globe. However, this is all relative to the camera and not the position of the globe. I am trying to set up a solution where it extracts the rotation of the globe, however I'm unable to get proper access to the globe object. I am using the ref property of the Globe component but when I check the ref, everything appears to be empty. Like the object structure is there, but the scene has no children or anything to dig into.

Is there a different way to go about this? Here is my custom Globe:


'use client'

import {useEffect, useRef, useState} from 'react';
import Globe from 'react-globe.gl';
import * as THREE from 'three';

const dayNightShader = {
    vertexShader: `
    varying vec3 vNormal;
    varying vec2 vUv;
    void main() {
      vNormal = normalize(normalMatrix * normal);
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
    fragmentShader: `
    #define PI 3.141592653589793

    uniform sampler2D dayTexture;
    uniform sampler2D nightTexture;
    uniform float time;
    uniform mat4 globeRotation;
    varying vec3 vNormal;
    varying vec2 vUv;
    
    void main() {
      // Calculate base sun angle from time
      float sunAngle = time * 2.0 * PI;
      
      // Create sun direction vector
      vec3 sunDirection = vec3(cos(sunAngle), 0.0, sin(sunAngle));
      
      // Apply globe rotation to sun direction
      vec3 rotatedSunDirection = (globeRotation * vec4(sunDirection, 0.0)).xyz;
      
      // Calculate intensity with rotated normal
      float intensity = dot(normalize(vNormal), normalize(rotatedSunDirection));
      
      // Mix textures based on calculated intensity
      vec4 dayColor = texture2D(dayTexture, vUv);
      vec4 nightColor = texture2D(nightTexture, vUv);
      float blendFactor = smoothstep(-0.1, 0.1, intensity);
      
      gl_FragColor = mix(nightColor, dayColor, blendFactor);
    }
  `,
};

interface Point {
    lat: number;
    lng: number;
    label: string;
}

const TestGlobe = () => {
    const globeRef = useRef<any>();
    const [timeOfDay] = useState(0.5);
    const [globeMaterial, setGlobeMaterial] = useState<THREE.ShaderMaterial>();
    const rotationMatrix = useRef(new THREE.Matrix4());

    // Initialize globe material
    useEffect(() => {
        const loader = new THREE.TextureLoader();

        Promise.all([
            loader.loadAsync('https://unpkg.com/three-globe/example/img/earth-day.jpg'),
            loader.loadAsync('https://unpkg.com/three-globe/example/img/earth-night.jpg')
        ]).then(([dayTexture, nightTexture]) => {
            console.log("Loaded day and night textures")

            const material = new THREE.ShaderMaterial({
                uniforms: {
                    time: { value: timeOfDay },
                    dayTexture: { value: dayTexture },
                    nightTexture: { value: nightTexture },
                    globeRotation: { value: new THREE.Matrix4() }
                },
                vertexShader: dayNightShader.vertexShader,
                fragmentShader: dayNightShader.fragmentShader,
                side: THREE.FrontSide
            });

            setGlobeMaterial(material);

        }).catch(err => console.error('Error loading day texture:', err));;
    }, [timeOfDay]);

    // Update rotation matrix and apply material
    useEffect(() => {
        let animationFrameId: number;
        let retries = 0;
        const MAX_RETRIES = 10;

        const updateRotation = () => {
            try {
                if (globeRef.current?.scene && globeMaterial) {
                    // Safely access scene children
                    const sceneChildren = globeRef.current.scene().children || [];
                    console.log(globeRef.current)
                    // Find globe mesh with more specific checks
                    const globeMesh = sceneChildren.find(
                        (obj: THREE.Object3D) =>
                            obj.type === 'Mesh' && obj.name.includes('globe')
                    );

                    if (globeMesh) {
                        // Update rotation matrix
                        rotationMatrix.current.extractRotation(globeMesh.matrixWorld);
                        globeMaterial.uniforms.globeRotation.value.copy(rotationMatrix.current);
                        globeMaterial.uniformsNeedUpdate = true;

                        // Apply material safely
                        if (!(globeMesh as THREE.Mesh).material) {
                            (globeMesh as THREE.Mesh).material = globeMaterial;
                        }
                    } else if (retries < MAX_RETRIES) {
                        retries++;
                        console.log('Globe mesh not found, retrying...');
                    }
                }
            } catch (error) {
                console.error('Error in updateRotation:', error);
            }

            animationFrameId = requestAnimationFrame(updateRotation);
        };

        // Initialize with identity matrix
        rotationMatrix.current.identity();
        updateRotation();

        return () => {
            cancelAnimationFrame(animationFrameId);
        };
    }, [globeMaterial]);

    const pointsData: Point[] = [
        { lat: 37.7749, lng: -122.4194, label: 'San Francisco' },
        { lat: 40.7128, lng: -74.006, label: 'New York' },
    ];

    return (
        <div style={{ width: '100%', height: '100vh' }}>
            <Globe
                ref={globeRef}
                width={window.innerWidth}
                height={window.innerHeight}
                backgroundColor="rgba(0, 0, 0, 0)"
                globeMaterial={globeMaterial}
                pointsData={pointsData}
                pointLabel="label"
                pointLat="lat"
                pointLng="lng"
                pointRadius={0.5}
                pointColor={() => 'red'}
            />
        </div>
    );
};

export default TestGlobe;

micarner avatar Feb 06 '25 22:02 micarner

Plus here's an image because I thought it looked cool!

Image

micarner avatar Feb 06 '25 22:02 micarner

@micarner this is very cool! I would love for there to be an example like this in this repo, so when you're finished with cleaning up the code feel free to submit a PR to add it. 😃

About getting the camera angle, you can get there easily like this:

const { lat, lng, altitude } = globeRef.current.pointOfView();

vasturiano avatar Feb 06 '25 23:02 vasturiano

Thanks man! Yes that worked great.

Yeah I'd love to add it once I get things set up. I've got it working but only 100% when the camera is at the equator. I need to doublecheck my math.

micarner avatar Feb 07 '25 02:02 micarner

Got it working. Was running smooth as butter until I tried to snag this short video.

https://github.com/user-attachments/assets/0e6d3f43-bd34-43fb-b4cd-49d03f368e8b

micarner avatar Feb 07 '25 04:02 micarner

Here is my final globe component for anyone else who needs to see it. It uses some hi res images I grabbed from here: https://planetpixelemporium.com/earth8081.html I tried some truly giant ones in 40k but they were like 500mb a piece and stuff started breaking.

I will check out your examples and try to whip something up that fits that format for the PR


'use client'

import { useEffect, useRef, useState } from 'react';
import Globe from 'react-globe.gl';
import { Slider } from "@/components/ui/slider";
import * as THREE from 'three';

const dayNightShader = {
    vertexShader: `
    varying vec3 vNormal;
    varying vec2 vUv;
    void main() {
      vNormal = normalize(normalMatrix * normal);
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
    fragmentShader: `
    // Fragment Shader Adjustments
    #define PI 3.141592653589793
    
    uniform sampler2D dayTexture;
    uniform sampler2D nightTexture;
    uniform float time;
    uniform vec3 globeRotation;  // Contains (lat, lng, 0)
    varying vec3 vNormal;
    varying vec2 vUv;
    
    void main() {
      // Base sun angle from time (fixed in world space)
      float sunAngle = time * 2.0 * PI;
      vec3 sunDirection = vec3(cos(sunAngle), 0.0, sin(sunAngle));
      
      // Convert to radians and invert rotations
      vec3 rotation = globeRotation * PI / 180.0;
      float invLat = -rotation.x;  // Inverse latitude rotation
      float invLon = rotation.y; // Inverse longitude rotation
    
      // Correct rotation order: latitude (X) first, then longitude (Y)
      mat3 rotX = mat3(
        1, 0, 0,
        0, cos(invLat), -sin(invLat),
        0, sin(invLat), cos(invLat)
      );
      
      mat3 rotY = mat3(
        cos(invLon), 0, sin(invLon),
        0, 1, 0,
        -sin(invLon), 0, cos(invLon)
      );
    
      // Apply inverse rotations in corrected order
      vec3 rotatedSunDirection = rotX * rotY * sunDirection;
      
      // Calculate intensity with rotated normals
      float intensity = dot(normalize(vNormal), normalize(rotatedSunDirection));
      
      // Mix textures
      vec4 dayColor = texture2D(dayTexture, vUv);
      vec4 nightColor = texture2D(nightTexture, vUv);
      float blendFactor = smoothstep(-0.1, 0.1, intensity);
      
      gl_FragColor = mix(nightColor, dayColor, blendFactor);
    }
  `,
};

interface Point {
    lat: number;
    lng: number;
    label: string;
}

const EarthGlobe = () => {
    const globeRef = useRef<any>();
    const [globeMaterial, setGlobeMaterial] = useState<THREE.ShaderMaterial>();
    const [globeRotation, setGlobeRotation] = useState({ lat: 0, lng: 0 });
    const startTimeRef = useRef(performance.now());
    const [cycleDuration, setCycleDuration] = useState(10); // Cycle duration in seconds

    useEffect(() => {
        const loader = new THREE.TextureLoader();

        Promise.all([
            loader.loadAsync('/globe/10k/day.jpg'),
            loader.loadAsync('/globe/10k/night.jpg'),
        ]).then(([dayTexture, nightTexture]) => {
            const material = new THREE.ShaderMaterial({
                uniforms: {
                    time: { value: 0 },
                    dayTexture: { value: dayTexture },
                    nightTexture: { value: nightTexture },
                    globeRotation: { value: new THREE.Vector3() }
                },
                vertexShader: dayNightShader.vertexShader,
                fragmentShader: dayNightShader.fragmentShader,
                side: THREE.FrontSide
            });

            setGlobeMaterial(material);
        });
    }, []);

    // Update rotation and timeOfDay
    useEffect(() => {
        let animationFrameId: number;

        const updateRotation = () => {
            if (globeRef.current && globeMaterial) {
                try {
                    // Update globe rotation
                    const { lat, lng } = globeRef.current.pointOfView();
                    setGlobeRotation({ lat, lng });
                    globeMaterial.uniforms.globeRotation.value.set(lat, lng, 0);

                    // Update day/night cycle
                    const currentTime = performance.now();
                    const elapsedTime = (currentTime - startTimeRef.current) / 1000; // Convert to seconds
                    const timeOfDay = (elapsedTime % cycleDuration) / cycleDuration;
                    globeMaterial.uniforms.time.value = timeOfDay;

                    globeMaterial.uniformsNeedUpdate = true;
                } catch (error) {
                    console.error('Error updating rotation:', error);
                }
            }
            animationFrameId = requestAnimationFrame(updateRotation);
        };

        updateRotation();
        return () => cancelAnimationFrame(animationFrameId);
    }, [globeMaterial, cycleDuration]);

    const pointsData = [
        { lat: 37.7749, lng: -122.4194, label: 'San Francisco' },
        { lat: 40.7128, lng: -74.006, label: 'New York' },
    ];

    return (
        <div style={{
            width: '100%',
            height: '100vh',
            position: 'relative',
            backgroundImage: "url('https://raw.githubusercontent.com/vasturiano/three-globe/refs/heads/master/example/img/night-sky.png')"
        }}>
            <Globe
                ref={globeRef}
                width={window.innerWidth}
                height={window.innerHeight}
                backgroundColor="rgba(0, 0, 0, 0)"
                globeMaterial={globeMaterial}
                pointsData={pointsData}
                pointLabel="label"
                pointLat="lat"
                pointLng="lng"
                pointRadius={0.1}
                pointColor={() => 'red'}
            />

            {/* Slider for adjusting cycleDuration */}
            <div style={{
                position: 'absolute',
                bottom: '20px',
                right: '20px',
                width: '200px',
                backgroundColor: 'rgba(255, 255, 255, 0.9)',
                padding: '10px',
                borderRadius: '8px',
                boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)'
            }}>
                <label htmlFor="cycle-duration-slider" style={{ display: 'block', marginBottom: '8px', fontSize: '14px', color: '#333' }}>
                    Cycle Duration (seconds): {cycleDuration}
                </label>
                <Slider
                    id="cycle-duration-slider"
                    value={[cycleDuration]}
                    min={5}
                    max={60}
                    step={1}
                    onValueChange={(value) => setCycleDuration(value[0])}
                />
            </div>
        </div>
    );
};

export default EarthGlobe;

micarner avatar Feb 07 '25 05:02 micarner

Here is that PR :)

https://github.com/vasturiano/react-globe.gl/pull/190

micarner avatar Feb 07 '25 20:02 micarner

How can we make this accurate geo position of sun so the shadow is real?

viprocket1 avatar Nov 05 '25 11:11 viprocket1

@viprocket1 as you can see from the code in the example https://github.com/vasturiano/react-globe.gl/blob/239b6ca9a50b7fa5dd07c68483e835dfc7893d68/example/day-night-cycle/index.html#L93-L98

it's using the package solar-calculator for computing accurate positions of the sun at any given time.

vasturiano avatar Nov 05 '25 12:11 vasturiano