Matrix Earth - Day 2
Advanced LevelThree.js abstracts WebGL complexity. We need a scene (container), camera (viewpoint), and renderer (draws to canvas).
// Import Three.js from CDN
import * as THREE from 'three';
// Create the scene - container for all objects
const scene = new THREE.Scene();
// Perspective camera: FOV, aspect ratio, near/far planes
const camera = new THREE.PerspectiveCamera(
60, // Field of view (degrees)
window.innerWidth / window.innerHeight, // Aspect ratio
0.1, // Near clipping plane
1000 // Far clipping plane
);
camera.position.z = 3; // Move camera back to see the globe
// WebGL renderer with antialiasing and transparency
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true // Transparent background
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
To distribute points evenly on a sphere, we use the Fibonacci spiral. Random placement creates clusters; Fibonacci ensures uniform coverage.
function fibonacciSphere(numPoints, radius) {
const points = [];
const phi = Math.PI * (3 - Math.sqrt(5)); // Golden angle ~2.4 rad
for (let i = 0; i < numPoints; i++) {
// Y goes from 1 to -1 (top to bottom of sphere)
const y = 1 - (i / (numPoints - 1)) * 2;
// Radius at this height (Pythagorean theorem)
const radiusAtY = Math.sqrt(1 - y * y);
// Golden angle increment
const theta = phi * i;
// Convert to Cartesian coordinates
const x = Math.cos(theta) * radiusAtY;
const z = Math.sin(theta) * radiusAtY;
points.push(new THREE.Vector3(
x * radius,
y * radius,
z * radius
));
}
return points;
}
The golden angle (137.5deg) is irrational, so spiral points never align perfectly - creating optimal spacing without clustering.
GLSL shaders run on the GPU, enabling effects like the Matrix falling characters. Vertex shader positions geometry; fragment shader colors pixels.
// Runs once per vertex
varying vec2 vUv; // Pass UV coords to fragment
varying float vElevation; // Pass height for coloring
void main() {
vUv = uv;
// Project vertex to screen space
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
// Calculate elevation for continent coloring
vElevation = position.y;
}
uniform float uTime; // Animated time value
varying vec2 vUv;
void main() {
// Create falling effect based on time
float fall = fract(vUv.y + uTime * 0.3);
// Fade out at bottom of "fall"
float alpha = smoothstep(0.0, 0.5, fall);
// Matrix green with glow
vec3 color = vec3(0.0, 1.0, 0.0) * (0.5 + 0.5 * alpha);
gl_FragColor = vec4(color, alpha);
}
We use a simplified polygon-based approach to detect if a point is on land or water, based on latitude/longitude.
// Simplified continent boundaries (lat/lon polygons)
const continents = {
northAmerica: [
{ lat: 70, lon: -170 },
{ lat: 70, lon: -50 },
{ lat: 15, lon: -80 },
// ... more points
],
// ... other continents
};
function isOnLand(lat, lon) {
for (const continent of Object.values(continents)) {
if (pointInPolygon(lat, lon, continent)) {
return true;
}
}
return false;
}
// Ray casting algorithm for point-in-polygon test
function pointInPolygon(lat, lon, polygon) {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const yi = polygon[i].lat, xi = polygon[i].lon;
const yj = polygon[j].lat, xj = polygon[j].lon;
if (((yi > lat) !== (yj > lat)) &&
(lon < (xj - xi) * (lat - yi) / (yj - yi) + xi)) {
inside = !inside;
}
}
return inside;
}
The glow is created with a slightly larger sphere using a custom shader that fades based on view angle.
// Atmosphere mesh - slightly larger than globe
const atmosphereGeometry = new THREE.SphereGeometry(1.15, 64, 64);
const atmosphereMaterial = new THREE.ShaderMaterial({
vertexShader: atmosphereVertexShader,
fragmentShader: `
varying vec3 vNormal;
void main() {
// Intensity based on view angle (Fresnel-like)
float intensity = pow(0.7 - dot(vNormal, vec3(0, 0, 1)), 2.0);
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0) * intensity;
}
`,
blending: THREE.AdditiveBlending, // Glow adds to background
side: THREE.BackSide, // Render inside of sphere
transparent: true
});
The pow(0.7 - dot(normal, viewDir), 2.0) creates edge glow - surfaces facing away from the camera appear brighter.
The globe auto-rotates and responds to user interaction (drag to rotate manually).
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Auto-rotation when not dragging
if (!isDragging) {
globe.rotation.y += 0.002;
}
// Update shader time uniform for Matrix effect
material.uniforms.uTime.value = performance.now() / 1000;
renderer.render(scene, camera);
}
// Mouse drag controls
canvas.addEventListener('mousedown', () => isDragging = true);
canvas.addEventListener('mouseup', () => isDragging = false);
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - previousMousePosition.x;
const deltaY = e.clientY - previousMousePosition.y;
globe.rotation.y += deltaX * 0.005;
globe.rotation.x += deltaY * 0.005;
previousMousePosition = { x: e.clientX, y: e.clientY };
});