Skip to content

Instantly share code, notes, and snippets.

@jamonholmgren
Created July 16, 2025 05:18
Show Gist options
  • Save jamonholmgren/d24465f4913194589f54358328fae643 to your computer and use it in GitHub Desktop.
Save jamonholmgren/d24465f4913194589f54358328fae643 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Earth and Moon 3D Simulation</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
overflow: hidden;
}
#canvas-container {
position: relative;
width: 100vw;
height: 100vh;
}
#controls {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px;
border-radius: 8px;
width: 300px;
max-height: 90vh;
overflow-y: auto;
}
#controls h3 {
margin-top: 0;
margin-bottom: 15px;
text-align: center;
}
.control-group {
margin-bottom: 12px;
}
.control-group label {
display: block;
margin-bottom: 4px;
font-size: 12px;
}
.control-group input[type="range"] {
width: 100%;
}
.value-display {
float: right;
font-size: 11px;
color: #aaa;
}
#instructions {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 5px;
font-size: 14px;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="canvas-container"></div>
<div id="instructions">Use mouse or touch to orbit, pan, zoom</div>
<div id="controls">
<h3>Simulation Controls</h3>
<div class="control-group">
<label
>Ambient Light Brightness
<span class="value-display" id="ambient-value">0.10</span></label
>
<input
type="range"
id="ambient-light"
min="0"
max="0.5"
value="0.1"
step="0.01"
/>
</div>
<div class="control-group">
<label
>Sun Brightness
<span class="value-display" id="sun-value">1.20</span></label
>
<input
type="range"
id="sun-brightness"
min="0"
max="3"
value="1.2"
step="0.1"
/>
</div>
<div class="control-group">
<label
>Earth Rotation Speed
<span class="value-display" id="rotation-value">0.10</span></label
>
<input
type="range"
id="earth-rotation"
min="0"
max="1"
value="0.1"
step="0.01"
/>
</div>
<div class="control-group">
<label
>Earth Normal Map Intensity
<span class="value-display" id="normal-value">0.50</span></label
>
<input
type="range"
id="earth-normal"
min="0"
max="2"
value="0.5"
step="0.1"
/>
</div>
<div class="control-group">
<label
>Earth Specular Intensity
<span class="value-display" id="specular-value">0.50</span></label
>
<input
type="range"
id="earth-specular"
min="0"
max="1"
value="0.5"
step="0.05"
/>
</div>
<div class="control-group">
<label
>City Lights Brightness
<span class="value-display" id="lights-value">1.00</span></label
>
<input
type="range"
id="city-lights"
min="0"
max="3"
value="1"
step="0.1"
/>
</div>
<div class="control-group">
<label
>Cloud Height
<span class="value-display" id="cloud-height-value">1.01</span></label
>
<input
type="range"
id="cloud-height"
min="1.005"
max="1.05"
value="1.01"
step="0.005"
/>
</div>
<div class="control-group">
<label
>Cloud Opacity
<span class="value-display" id="cloud-opacity-value"
>0.80</span
></label
>
<input
type="range"
id="cloud-opacity"
min="0"
max="1"
value="0.8"
step="0.05"
/>
</div>
<div class="control-group">
<label
>Moon Normal Map Intensity
<span class="value-display" id="moon-normal-value">0.80</span></label
>
<input
type="range"
id="moon-normal"
min="0"
max="2"
value="0.8"
step="0.1"
/>
</div>
<div class="control-group">
<label
>Star Brightness
<span class="value-display" id="star-value">1.00</span></label
>
<input
type="range"
id="star-brightness"
min="0"
max="2"
value="1"
step="0.1"
/>
</div>
</div>
<script type="module">
const assetHost = "https://todds.sfo3.digitaloceanspaces.com";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
10000
);
camera.position.set(0, 0, 30);
// Renderer setup with high quality settings
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance",
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
document
.getElementById("canvas-container")
.appendChild(renderer.domElement);
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 15;
controls.maxDistance = 100;
// Texture loader
const textureLoader = new THREE.TextureLoader();
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.1);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
sunLight.position.set(50, 0, 0);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 4096;
sunLight.shadow.mapSize.height = 4096;
sunLight.shadow.camera.near = 0.1;
sunLight.shadow.camera.far = 200;
sunLight.shadow.camera.left = -50;
sunLight.shadow.camera.right = 50;
sunLight.shadow.camera.top = 50;
sunLight.shadow.camera.bottom = -50;
scene.add(sunLight);
// Earth group
const earthGroup = new THREE.Group();
scene.add(earthGroup);
// Load all textures
const earthDayTexture = textureLoader.load(
assetHost + "/earth-day-10k.jpg"
);
const earthNightTexture = textureLoader.load(
assetHost + "/earth-night-no-lights-10k.jpg"
);
const earthLightsTexture = textureLoader.load(
assetHost + "/earth-night-only-lights-10k.png"
);
const earthNormalMap = textureLoader.load(
assetHost + "/earth-normal-map-8k.jpg"
);
const earthSpecularMap = textureLoader.load(
assetHost + "/earth-specular-map-10k.jpg"
);
const cloudTexture = textureLoader.load(assetHost + "/clouds-8k.png");
const moonTexture = textureLoader.load(assetHost + "/moon-8k.jpg");
const moonNormalMap = textureLoader.load(
assetHost + "/moon-normal-map-4k.jpg"
);
const starTexture = textureLoader.load(assetHost + "/starmap_16k.jpg");
// Set texture parameters for quality
[
earthDayTexture,
earthNightTexture,
earthLightsTexture,
earthNormalMap,
earthSpecularMap,
cloudTexture,
moonTexture,
moonNormalMap,
starTexture,
].forEach((texture) => {
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
});
// Earth radius
const earthRadius = 10;
// Custom shader material for Earth with day/night cycle and city lights
const earthMaterial = new THREE.ShaderMaterial({
uniforms: {
dayTexture: { value: earthDayTexture },
nightTexture: { value: earthNightTexture },
lightsTexture: { value: earthLightsTexture },
normalMap: { value: earthNormalMap },
specularMap: { value: earthSpecularMap },
normalScale: { value: 0.5 },
specularIntensity: { value: 0.5 },
lightsIntensity: { value: 1.0 },
sunDirection: { value: new THREE.Vector3(1, 0, 0) },
ambientStrength: { value: 0.1 },
},
vertexShader: `
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
varying vec3 vTangent;
varying vec3 vBitangent;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;
vec3 tangent = vec3(1.0, 0.0, 0.0);
vTangent = normalize(normalMatrix * tangent);
vBitangent = cross(vNormal, vTangent);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D dayTexture;
uniform sampler2D nightTexture;
uniform sampler2D lightsTexture;
uniform sampler2D normalMap;
uniform sampler2D specularMap;
uniform float normalScale;
uniform float specularIntensity;
uniform float lightsIntensity;
uniform vec3 sunDirection;
uniform float ambientStrength;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;
varying vec3 vTangent;
varying vec3 vBitangent;
void main() {
// Normal mapping
vec3 normalTex = texture2D(normalMap, vUv).xyz * 2.0 - 1.0;
normalTex.xy *= normalScale;
mat3 tbn = mat3(vTangent, vBitangent, vNormal);
vec3 normal = normalize(tbn * normalTex);
// Calculate sun direction in view space
vec3 sunDir = normalize((viewMatrix * vec4(sunDirection, 0.0)).xyz);
// Lighting calculation
float sunDot = dot(normal, sunDir);
float dayStrength = smoothstep(-0.3, 0.3, sunDot);
// Sample textures
vec3 dayColor = texture2D(dayTexture, vUv).rgb;
vec3 nightColor = texture2D(nightTexture, vUv).rgb;
vec4 lightsColor = texture2D(lightsTexture, vUv);
float specular = texture2D(specularMap, vUv).r;
// Mix day and night
vec3 color = mix(nightColor, dayColor, dayStrength);
// Add city lights (only visible on night side)
float nightStrength = 1.0 - dayStrength;
float lightsVisibility = smoothstep(0.3, 0.7, nightStrength);
color += lightsColor.rgb * lightsColor.a * lightsIntensity * lightsVisibility;
// Specular reflection (only on water/oceans)
vec3 viewDir = normalize(-vPosition);
vec3 reflectDir = reflect(-sunDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
color += vec3(1.0) * spec * specular * specularIntensity * dayStrength;
// Add ambient light
color += dayColor * ambientStrength;
gl_FragColor = vec4(color, 1.0);
}
`,
});
// Create Earth
const earthGeometry = new THREE.SphereGeometry(earthRadius, 128, 64);
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
earth.castShadow = true;
earth.receiveShadow = true;
earthGroup.add(earth);
// Atmosphere
const atmosphereGeometry = new THREE.SphereGeometry(
earthRadius * 1.015,
128,
64
);
const atmosphereMaterial = new THREE.ShaderMaterial({
uniforms: {
sunDirection: { value: new THREE.Vector3(1, 0, 0) },
},
vertexShader: `
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vNormal = normalize(normalMatrix * normal);
vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 sunDirection;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vec3 sunDir = normalize((viewMatrix * vec4(sunDirection, 0.0)).xyz);
float intensity = pow(0.8 - dot(vNormal, normalize(-vPosition)), 2.0);
// Add sun-side glow
float sunDot = dot(vNormal, sunDir);
float sunGlow = smoothstep(-0.3, 0.7, sunDot) * 0.3;
vec3 atmosphereColor = vec3(0.3, 0.6, 1.0);
float alpha = intensity * 0.6 + sunGlow;
gl_FragColor = vec4(atmosphereColor, alpha);
}
`,
transparent: true,
side: THREE.BackSide,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
earthGroup.add(atmosphere);
// Clouds
const cloudGeometry = new THREE.SphereGeometry(
earthRadius * 1.01,
128,
64
);
const cloudMaterial = new THREE.MeshPhongMaterial({
map: cloudTexture,
transparent: true,
opacity: 0.8,
depthWrite: false,
side: THREE.DoubleSide,
});
const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial);
clouds.castShadow = true;
earthGroup.add(clouds);
// Moon
const moonRadius = earthRadius * 0.273;
const moonDistance = 40;
const moonGeometry = new THREE.SphereGeometry(moonRadius, 64, 32);
const moonMaterial = new THREE.MeshPhongMaterial({
map: moonTexture,
normalMap: moonNormalMap,
normalScale: new THREE.Vector2(0.8, 0.8),
shininess: 5,
});
const moon = new THREE.Mesh(moonGeometry, moonMaterial);
moon.castShadow = true;
moon.receiveShadow = true;
// Moon group for orbit
const moonGroup = new THREE.Group();
moonGroup.add(moon);
moon.position.x = moonDistance;
scene.add(moonGroup);
// Starfield
const starGeometry = new THREE.SphereGeometry(500, 64, 32);
const starMaterial = new THREE.MeshBasicMaterial({
map: starTexture,
side: THREE.BackSide,
});
const starfield = new THREE.Mesh(starGeometry, starMaterial);
scene.add(starfield);
// UI Controls
const controls_ui = {
ambientLight: document.getElementById("ambient-light"),
sunBrightness: document.getElementById("sun-brightness"),
earthRotation: document.getElementById("earth-rotation"),
earthNormal: document.getElementById("earth-normal"),
earthSpecular: document.getElementById("earth-specular"),
cityLights: document.getElementById("city-lights"),
cloudHeight: document.getElementById("cloud-height"),
cloudOpacity: document.getElementById("cloud-opacity"),
moonNormal: document.getElementById("moon-normal"),
starBrightness: document.getElementById("star-brightness"),
};
// Value displays
const valueDisplays = {
ambient: document.getElementById("ambient-value"),
sun: document.getElementById("sun-value"),
rotation: document.getElementById("rotation-value"),
normal: document.getElementById("normal-value"),
specular: document.getElementById("specular-value"),
lights: document.getElementById("lights-value"),
cloudHeight: document.getElementById("cloud-height-value"),
cloudOpacity: document.getElementById("cloud-opacity-value"),
moonNormal: document.getElementById("moon-normal-value"),
star: document.getElementById("star-value"),
};
// Simulation parameters
let earthRotationSpeed = 0.1;
let cloudRotationMultiplier = 1.2;
// UI event listeners
controls_ui.ambientLight.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
ambientLight.intensity = value;
earthMaterial.uniforms.ambientStrength.value = value;
valueDisplays.ambient.textContent = value.toFixed(2);
});
controls_ui.sunBrightness.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
sunLight.intensity = value;
valueDisplays.sun.textContent = value.toFixed(2);
});
controls_ui.earthRotation.addEventListener("input", (e) => {
earthRotationSpeed = parseFloat(e.target.value);
valueDisplays.rotation.textContent = earthRotationSpeed.toFixed(2);
});
controls_ui.earthNormal.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
earthMaterial.uniforms.normalScale.value = value;
valueDisplays.normal.textContent = value.toFixed(2);
});
controls_ui.earthSpecular.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
earthMaterial.uniforms.specularIntensity.value = value;
valueDisplays.specular.textContent = value.toFixed(2);
});
controls_ui.cityLights.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
earthMaterial.uniforms.lightsIntensity.value = value;
valueDisplays.lights.textContent = value.toFixed(2);
});
controls_ui.cloudHeight.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
clouds.scale.setScalar(value);
valueDisplays.cloudHeight.textContent = value.toFixed(3);
});
controls_ui.cloudOpacity.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
cloudMaterial.opacity = value;
valueDisplays.cloudOpacity.textContent = value.toFixed(2);
});
controls_ui.moonNormal.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
moonMaterial.normalScale.set(value, value);
valueDisplays.moonNormal.textContent = value.toFixed(2);
});
controls_ui.starBrightness.addEventListener("input", (e) => {
const value = parseFloat(e.target.value);
starMaterial.color.setScalar(value);
valueDisplays.star.textContent = value.toFixed(2);
});
// Animation
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
// Rotate Earth and clouds
earth.rotation.y += earthRotationSpeed * deltaTime;
clouds.rotation.y +=
earthRotationSpeed * cloudRotationMultiplier * deltaTime;
// Moon orbit (tidally locked)
const moonOrbitSpeed = earthRotationSpeed * 0.0366; // Moon orbits once per ~27.3 Earth days
moonGroup.rotation.y += moonOrbitSpeed * deltaTime;
moon.rotation.y = -moonGroup.rotation.y; // Keep same face toward Earth
// Update sun direction for shaders
const sunAngle = 0;
const sunDir = new THREE.Vector3(
Math.cos(sunAngle),
0,
Math.sin(sunAngle)
);
earthMaterial.uniforms.sunDirection.value = sunDir;
atmosphereMaterial.uniforms.sunDirection.value = sunDir;
controls.update();
renderer.render(scene, camera);
}
// Handle window resize
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start animation
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment