Day/night cycle implementation - how to get scene from globe ref?
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;
Plus here's an image because I thought it looked cool!
@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();
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.
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
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;
Here is that PR :)
https://github.com/vasturiano/react-globe.gl/pull/190
How can we make this accurate geo position of sun so the shadow is real?
@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.