Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
micarner opened this issue Feb 6, 2025 · 6 comments
Open

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

micarner opened this issue Feb 6, 2025 · 6 comments

Comments

@micarner
Copy link
Contributor

micarner commented Feb 6, 2025

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
Copy link
Contributor Author

micarner commented Feb 6, 2025

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

Image

@vasturiano
Copy link
Owner

@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();

@micarner
Copy link
Contributor Author

micarner commented Feb 7, 2025

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
Copy link
Contributor Author

micarner commented Feb 7, 2025

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

react-globe.day.night.mp4

@micarner
Copy link
Contributor Author

micarner commented Feb 7, 2025

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
Copy link
Contributor Author

micarner commented Feb 7, 2025

Here is that PR :)

#190

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants