From e504ffbec657cacb84a36fcdb3eb35d882ee0722 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 01:08:18 -0400 Subject: [PATCH] feat: holographic planet Earth slowly rotating above the Nexus (#253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds earthGroup positioned at y=20 with Earth's 23.4° axial tilt - Custom GLSL shader: layered simplex noise for continent shapes, ocean/land coloring with holographic cyan tint, animated scan lines, fresnel rim glow - Lat/lon grid wireframe (every 30°) for classic holographic globe look - Additive atmosphere shell for soft outer glow - PointLight pulses gently in sync with Earth's glow - Slow axial rotation (0.035 rad/s) in animate() loop - zoomLabel 'Planet Earth' for double-click zoom support Fixes #253 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/app.js b/app.js index 4a44cb8..f70d7df 100644 --- a/app.js +++ b/app.js @@ -967,6 +967,198 @@ for (let i = 0; i < RUNE_COUNT; i++) { } +// === HOLOGRAPHIC EARTH === +// A procedural holographic planet Earth slowly rotating above the Nexus. +// Continents rendered via layered simplex noise on UV-sphere coords. +// Holographic effects: scan lines, fresnel rim glow, lat/lon grid overlay. + +const EARTH_RADIUS = 2.8; +const EARTH_Y = 20.0; +const EARTH_ROTATION_SPEED = 0.035; // radians per second — gentle drift +const EARTH_AXIAL_TILT = 23.4 * (Math.PI / 180); + +const earthGroup = new THREE.Group(); +earthGroup.position.set(0, EARTH_Y, 0); +earthGroup.rotation.z = EARTH_AXIAL_TILT; +scene.add(earthGroup); + +// Surface shader — continents via noise, holographic scan lines, fresnel rim +const earthSurfaceMat = new THREE.ShaderMaterial({ + uniforms: { + uTime: { value: 0.0 }, + uOceanColor: { value: new THREE.Color(0x003d99) }, + uLandColor: { value: new THREE.Color(0x1a5c2a) }, + uGlowColor: { value: new THREE.Color(NEXUS.colors.accent) }, + }, + vertexShader: ` + varying vec3 vNormal; + varying vec3 vWorldPos; + varying vec2 vUv; + void main() { + vNormal = normalize(normalMatrix * normal); + vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz; + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform float uTime; + uniform vec3 uOceanColor; + uniform vec3 uLandColor; + uniform vec3 uGlowColor; + varying vec3 vNormal; + varying vec3 vWorldPos; + varying vec2 vUv; + + // Simplex noise (3-D) + vec3 _m3(vec3 x){ return x - floor(x*(1./289.))*289.; } + vec4 _m4(vec4 x){ return x - floor(x*(1./289.))*289.; } + vec4 _p4(vec4 x){ return _m4((x*34.+1.)*x); } + float snoise(vec3 v){ + const vec2 C = vec2(1./6., 1./3.); + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + vec3 x1 = x0 - i1 + C.xxx; + vec3 x2 = x0 - i2 + C.yyy; + vec3 x3 = x0 - 0.5; + i = _m3(i); + vec4 p = _p4(_p4(_p4( + i.z+vec4(0.,i1.z,i2.z,1.))+ + i.y+vec4(0.,i1.y,i2.y,1.))+ + i.x+vec4(0.,i1.x,i2.x,1.)); + float n_ = .142857142857; + vec3 ns = n_*vec3(2.,0.,-1.)+vec3(0.,-.5,1.); + vec4 j = p - 49.*floor(p*ns.z*ns.z); + vec4 x_ = floor(j*ns.z); + vec4 y_ = floor(j - 7.*x_); + vec4 h = 1. - abs(x_*(2./7.)) - abs(y_*(2./7.)); + vec4 b0 = vec4(x_.xy,y_.xy)*(2./7.); + vec4 b1 = vec4(x_.zw,y_.zw)*(2./7.); + vec4 s0 = floor(b0)*2.+1.; vec4 s1 = floor(b1)*2.+1.; + vec4 sh = -step(h, vec4(0.)); + vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww; + vec3 p0=vec3(a0.xy,h.x); vec3 p1=vec3(a0.zw,h.y); + vec3 p2=vec3(a1.xy,h.z); vec3 p3=vec3(a1.zw,h.w); + vec4 nm = max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.); + vec4 nr = 1.79284291400159-0.85373472095314*nm; + p0*=nr.x; p1*=nr.y; p2*=nr.z; p3*=nr.w; + nm = nm*nm; + return 42.*dot(nm*nm, vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3))); + } + + void main() { + vec3 n = normalize(vNormal); + vec3 vd = normalize(cameraPosition - vWorldPos); + + // Stable spherical sample coords from UV (fixed to sphere surface) + float lat = (vUv.y - 0.5) * 3.14159265; + float lon = vUv.x * 6.28318530; + vec3 sp = vec3(cos(lat)*cos(lon), sin(lat), cos(lat)*sin(lon)); + + // Layered noise for continent shapes + float c = snoise(sp*1.8)*0.60 + + snoise(sp*3.6)*0.30 + + snoise(sp*7.2)*0.10; + float land = smoothstep(0.05, 0.30, c); + + // Surface colour: ocean / land, holographic-tinted + vec3 surf = mix(uOceanColor, uLandColor, land); + surf = mix(surf, uGlowColor * 0.45, 0.38); + + // Scan lines (horizontal bands) + float scan = 0.5 + 0.5*sin(vUv.y * 220.0 + uTime * 1.8); + scan = smoothstep(0.30, 0.70, scan) * 0.14; + + // Fresnel rim glow + float fresnel = pow(1.0 - max(dot(n, vd), 0.0), 4.0); + + vec3 col = surf + scan*uGlowColor*0.9 + fresnel*uGlowColor*1.5; + float alpha = 0.48 + fresnel * 0.42; + + gl_FragColor = vec4(col, alpha); + } + `, + transparent: true, + depthWrite: false, + side: THREE.FrontSide, +}); + +const earthSphere = new THREE.SphereGeometry(EARTH_RADIUS, 64, 32); +const earthMesh = new THREE.Mesh(earthSphere, earthSurfaceMat); +earthMesh.userData.zoomLabel = 'Planet Earth'; +earthGroup.add(earthMesh); + +// Lat/lon grid lines +(function buildEarthGrid() { + const lineMat = new THREE.LineBasicMaterial({ + color: 0x2266bb, + transparent: true, + opacity: 0.30, + }); + const r = EARTH_RADIUS + 0.015; + const SEG = 64; + + // Latitude rings every 30° + for (let lat = -60; lat <= 60; lat += 30) { + const phi = lat * (Math.PI / 180); + const pts = []; + for (let i = 0; i <= SEG; i++) { + const th = (i / SEG) * Math.PI * 2; + pts.push(new THREE.Vector3( + Math.cos(phi) * Math.cos(th) * r, + Math.sin(phi) * r, + Math.cos(phi) * Math.sin(th) * r + )); + } + earthGroup.add(new THREE.Line( + new THREE.BufferGeometry().setFromPoints(pts), lineMat + )); + } + + // Longitude meridians every 30° + for (let lon = 0; lon < 360; lon += 30) { + const th = lon * (Math.PI / 180); + const pts = []; + for (let i = 0; i <= SEG; i++) { + const phi = (i / SEG) * Math.PI - Math.PI / 2; + pts.push(new THREE.Vector3( + Math.cos(phi) * Math.cos(th) * r, + Math.sin(phi) * r, + Math.cos(phi) * Math.sin(th) * r + )); + } + earthGroup.add(new THREE.Line( + new THREE.BufferGeometry().setFromPoints(pts), lineMat + )); + } +})(); + +// Atmosphere shell — soft additive glow around the globe +const atmMat = new THREE.MeshBasicMaterial({ + color: 0x1144cc, + transparent: true, + opacity: 0.07, + side: THREE.BackSide, + depthWrite: false, + blending: THREE.AdditiveBlending, +}); +earthGroup.add(new THREE.Mesh( + new THREE.SphereGeometry(EARTH_RADIUS * 1.14, 32, 16), atmMat +)); + +// Soft blue point light emanating from Earth +const earthGlowLight = new THREE.PointLight(NEXUS.colors.accent, 0.4, 25); +earthGroup.add(earthGlowLight); + +earthGroup.traverse(obj => { + if (obj.isMesh || obj.isLine) obj.userData.zoomLabel = 'Planet Earth'; +}); + // === WARP TUNNEL EFFECT === const WarpShader = { uniforms: { @@ -1185,6 +1377,11 @@ function animate() { rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2; } + // Animate holographic Earth — slow axial rotation, glow pulse + earthMesh.rotation.y = elapsed * EARTH_ROTATION_SPEED; + earthSurfaceMat.uniforms.uTime.value = elapsed; + earthGlowLight.intensity = 0.30 + Math.sin(elapsed * 0.7) * 0.12; + // === WEATHER PARTICLE ANIMATION === if (rainParticles.visible) { const rpos = rainGeo.attributes.position.array; -- 2.43.0