LOADING EARTH ONLINE

地球 ONLINEEARTH · SIMULATION
FPS0
UTC00:00:00
在线0

★ 全球节点

×

-

-
纬度-
经度-
人口-
时区-
本地时间-
在线用户-
提示: 拖拽旋转 · 滚轮缩放 · 点击城市查看详情
/* =========================================================== 全球主要城市 =========================================================== */ const CITIES = [ {n:"北京", e:"Beijing", c:"中国", cc:"cn", lat:39.9042, lng:116.4074, pop:21893000, tz:"UTC+8"}, {n:"上海", e:"Shanghai", c:"中国", cc:"cn", lat:31.2304, lng:121.4737, pop:24870000, tz:"UTC+8"}, {n:"东京", e:"Tokyo", c:"日本", cc:"jp", lat:35.6762, lng:139.6503, pop:13960000, tz:"UTC+9"}, {n:"首尔", e:"Seoul", c:"韩国", cc:"kr", lat:37.5665, lng:126.9780, pop:9776000, tz:"UTC+9"}, {n:"新加坡", e:"Singapore", c:"新加坡", cc:"sg", lat:1.3521, lng:103.8198, pop:5685000, tz:"UTC+8"}, {n:"孟买", e:"Mumbai", c:"印度", cc:"in", lat:19.0760, lng:72.8777, pop:20410000, tz:"UTC+5:30"}, {n:"迪拜", e:"Dubai", c:"阿联酋", cc:"ae", lat:25.2048, lng:55.2708, pop:3331000, tz:"UTC+4"}, {n:"伊斯坦布尔",e:"Istanbul", c:"土耳其", cc:"tr", lat:41.0082, lng:28.9784, pop:15462000, tz:"UTC+3"}, {n:"莫斯科", e:"Moscow", c:"俄罗斯", cc:"ru", lat:55.7558, lng:37.6173, pop:12506000, tz:"UTC+3"}, {n:"伦敦", e:"London", c:"英国", cc:"gb", lat:51.5074, lng:-0.1278, pop:9540000, tz:"UTC+0"}, {n:"巴黎", e:"Paris", c:"法国", cc:"fr", lat:48.8566, lng:2.3522, pop:11020000, tz:"UTC+1"}, {n:"柏林", e:"Berlin", c:"德国", cc:"de", lat:52.5200, lng:13.4050, pop:3645000, tz:"UTC+1"}, {n:"罗马", e:"Rome", c:"意大利", cc:"it", lat:41.9028, lng:12.4964, pop:4316000, tz:"UTC+1"}, {n:"马德里", e:"Madrid", c:"西班牙", cc:"es", lat:40.4168, lng:-3.7038, pop:6640000, tz:"UTC+1"}, {n:"开罗", e:"Cairo", c:"埃及", cc:"eg", lat:30.0444, lng:31.2357, pop:9540000, tz:"UTC+2"}, {n:"纽约", e:"New York", c:"美国", cc:"us", lat:40.7128, lng:-74.0060, pop:18867000, tz:"UTC-5"}, {n:"洛杉矶", e:"Los Angeles", c:"美国", cc:"us", lat:34.0522, lng:-118.2437,pop:12459000, tz:"UTC-8"}, {n:"芝加哥", e:"Chicago", c:"美国", cc:"us", lat:41.8781, lng:-87.6298, pop:8901000, tz:"UTC-6"}, {n:"多伦多", e:"Toronto", c:"加拿大", cc:"ca", lat:43.6532, lng:-79.3832, pop:6313000, tz:"UTC-5"}, {n:"墨西哥城",e:"Mexico City", c:"墨西哥", cc:"mx", lat:19.4326, lng:-99.1332, pop:9210000, tz:"UTC-6"}, {n:"圣保罗", e:"São Paulo", c:"巴西", cc:"br", lat:-23.5505,lng:-46.6333,pop:22429000, tz:"UTC-3"}, {n:"布宜诺斯艾利斯",e:"Buenos Aires",c:"阿根廷",cc:"ar",lat:-34.6037,lng:-58.3816,pop:15370000,tz:"UTC-3"}, {n:"悉尼", e:"Sydney", c:"澳大利亚", cc:"au", lat:-33.8688,lng:151.2093,pop:5312000, tz:"UTC+11"}, {n:"墨尔本", e:"Melbourne", c:"澳大利亚", cc:"au", lat:-37.8136,lng:144.9631,pop:5078000, tz:"UTC+11"}, {n:"约翰内斯堡",e:"Johannesburg",c:"南非", cc:"za", lat:-26.2041,lng:28.0473, pop:6065000, tz:"UTC+2"}, {n:"开普敦",e:"Cape Town", c:"南非", cc:"za", lat:-33.9249,lng:18.4241, pop:4617000, tz:"UTC+2"}, {n:"雅加达", e:"Jakarta", c:"印度尼西亚",cc:"id",lat:-6.2088,lng:106.8456,pop:10770000, tz:"UTC+7"}, {n:"曼谷", e:"Bangkok", c:"泰国", cc:"th", lat:13.7563, lng:100.5018, pop:10539000, tz:"UTC+7"}, {n:"利雅得", e:"Riyadh", c:"沙特", cc:"sa", lat:24.7136, lng:46.6753, pop:7676000, tz:"UTC+3"}, {n:"好莱坞",e:"Hollywood", c:"美国", cc:"us", lat:34.0928, lng:-118.3287,pop:86000, tz:"UTC-8"}, ]; /* =========================================================== 工具函数 =========================================================== */ const $ = s => document.querySelector(s); const rand = (a,b)=>a+Math.random()*(b-a); const fmt = n => n.toString().padStart(2,'0'); function latLngToVec3(lat, lng, r){ const phi = (90 - lat) * Math.PI / 180; const theta = (lng + 180) * Math.PI / 180; return new THREE.Vector3( -r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta) ); } function curveBetween(p1, p2, r, h){ const dist = p1.distanceTo(p2); const mid = p1.clone().add(p2).multiplyScalar(0.5); mid.normalize().multiplyScalar(r + h + dist*0.3); return new THREE.QuadraticBezierCurve3(p1, mid, p2); } /* =========================================================== 程序化生成地球纹理 =========================================================== */ function makeEarthTexture(w=1024, h=512){ const c = document.createElement('canvas'); c.width=w; c.height=h; const ctx = c.getContext('2d'); // 海洋 const grad = ctx.createLinearGradient(0,0,0,h); grad.addColorStop(0,'#0a2540'); grad.addColorStop(0.5,'#0d3a66'); grad.addColorStop(1,'#0a2540'); ctx.fillStyle = grad; ctx.fillRect(0,0,w,h); // 大陆(用噪声 + 大致真实地理轮廓) const img = ctx.getImageData(0,0,w,h); const d = img.data; // 简化的"大陆分布" - 用椭圆形 + 噪声模拟 const continents = [ // 北美 {cx:0.22, cy:0.40, rx:0.18, ry:0.20}, // 南美 {cx:0.30, cy:0.68, rx:0.10, ry:0.18}, // 欧洲 {cx:0.50, cy:0.34, rx:0.08, ry:0.10}, // 非洲 {cx:0.53, cy:0.58, rx:0.10, ry:0.18}, // 亚洲 {cx:0.68, cy:0.38, rx:0.20, ry:0.18}, // 澳大利亚 {cx:0.83, cy:0.70, rx:0.07, ry:0.06}, // 南极 {cx:0.50, cy:0.95, rx:0.50, ry:0.08}, ]; for(let y=0;y 0){ // 陆地颜色:低地=绿,高地=棕 const elev = isLand; if(elev < 0.3){ d[i] = 60 + elev*100; d[i+1] = 100 + elev*100; d[i+2] = 40 + elev*60; } else if(elev < 0.7){ d[i] = 120 + elev*60; d[i+1] = 110 + elev*40; d[i+2] = 60 + elev*30; } else { d[i] = 180; d[i+1] = 160; d[i+2] = 130; } // 沙漠(撒哈拉、阿拉伯) if((u>0.42 && u<0.55 && v>0.45 && v<0.60) || (u>0.60 && u<0.70 && v>0.42 && v<0.55)){ d[i] = 210; d[i+1] = 190; d[i+2] = 130; } // 极地白 if(v < 0.08 || v > 0.92){ d[i] = 230; d[i+1] = 235; d[i+2] = 240; } } else { // 海洋细节 const wv = Math.sin(u*40)*0.5 + Math.sin(v*30)*0.5; d[i+2] = Math.max(20, d[i+2] + wv*8); } } } ctx.putImageData(img,0,0); return new THREE.CanvasTexture(c); } function makeBumpTexture(w=512, h=256){ const c = document.createElement('canvas'); c.width=w; c.height=h; const ctx = c.getContext('2d'); const img = ctx.createImageData(w,h); const d = img.data; for(let y=0;y THREE.Texture const flagMeshes = []; // 所有 3D 国旗平面(每帧更新朝向) const flagVertexShader = ` uniform float uTime; uniform float uWave; // 飘动强度(点击时增大) varying vec2 vUv; void main(){ vUv = uv; vec3 p = position; // 旗帜飘动:水平方向按 x 位置相位偏移,向上扩散 float k = uv.x; float h = uv.y; // 旗杆在 x=0 float w1 = sin(p.x*9.0 - uTime*3.5) * 0.020 * h; float w2 = sin(p.x*15.0 - uTime*5.0 + 1.0) * 0.012 * h; float w3 = cos(p.x*4.0 - uTime*1.2) * 0.010 * h; p.z += (w1 + w2 + w3) * uWave; // 旗杆处的轻微倾斜 p.z += sin(p.y*2.0 - uTime*2.0) * 0.006 * h; gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0); } `; const flagFragmentShader = ` uniform sampler2D uTex; uniform vec3 uTint; varying vec2 vUv; void main(){ vec4 c = texture2D(uTex, vUv); // 模拟光照:旗杆处稍亮,旗尾稍暗 float shade = 0.85 + 0.25 * (1.0 - vUv.x) + 0.05 * sin(vUv.y*8.0); gl_FragColor = vec4(c.rgb * shade * uTint, c.a); } `; function loadFlag(cc){ if(flagCache[cc]) return Promise.resolve(flagCache[cc]); return new Promise((resolve)=>{ const img = new Image(); img.onload = ()=>{ const tex = new THREE.Texture(img); tex.needsUpdate = true; tex.minFilter = THREE.LinearFilter; tex.magFilter = THREE.LinearFilter; flagCache[cc] = tex; resolve(tex); }; img.onerror = ()=>{ // 失败时给一个占位(纯灰) const c = document.createElement('canvas'); c.width=c.height=2; const x = c.getContext('2d'); x.fillStyle='#888'; x.fillRect(0,0,2,2); const tex = new THREE.CanvasTexture(c); flagCache[cc] = tex; resolve(tex); }; img.src = `flags/${cc}.png`; }); } const sharedFlagGeo = new THREE.PlaneGeometry(0.10, 0.067, 20, 10); const flagPoleGeo = new THREE.CylinderGeometry(0.0015, 0.0015, 0.12, 6); CITIES.forEach(city=>{ const p = latLngToVec3(city.lat, city.lng, R*1.005); // 圆点 const dot = new THREE.Mesh( new THREE.SphereGeometry(0.012, 8, 8), new THREE.MeshBasicMaterial({color:0x66ccff}) ); dot.position.copy(p); dot.userData = city; cityGroup.add(dot); cityMeshes.push(dot); // 光环(脉冲) const ring = new THREE.Mesh( new THREE.RingGeometry(0.012, 0.020, 24), new THREE.MeshBasicMaterial({color:0x66ccff, transparent:true, opacity:0.6, side:THREE.DoubleSide}) ); ring.position.copy(p.clone().multiplyScalar(1.001)); ring.lookAt(0,0,0); ring.userData = {pulse:Math.random()*Math.PI*2, baseScale:1}; cityGroup.add(ring); cityMeshes.push(ring); // 国旗旗杆(细柱) const pole = new THREE.Mesh(flagPoleGeo, new THREE.MeshBasicMaterial({color:0xcccccc})); pole.position.copy(p.clone().multiplyScalar(1.002)); pole.lookAt(0,0,0); pole.rotateX(Math.PI/2); pole.position.add(p.clone().normalize().multiplyScalar(0.05)); cityGroup.add(pole); cityMeshes.push(pole); // 异步加载国旗贴图 loadFlag(city.cc).then(tex=>{ const mat = new THREE.ShaderMaterial({ uniforms:{ uTime:{value:0}, uTex:{value:tex}, uWave:{value:1.0}, uTint:{value:new THREE.Color(0xffffff)}, }, vertexShader:flagVertexShader, fragmentShader:flagFragmentShader, transparent:true, side:THREE.DoubleSide, }); const flag = new THREE.Mesh(sharedFlagGeo, mat); flag.userData = {city, isFlag:true, waveBoost:0}; // 位置:沿城市法线方向抬起,作为旗帜顶端 flag.position.copy(p.clone().normalize().multiplyScalar(R*1.005 + 0.085)); cityGroup.add(flag); flagMeshes.push(flag); cityMeshes.push(flag); }); }); /* 连线(程序化生成) */ const lineGroup = new THREE.Group(); earthGroup.add(lineGroup); const lines=[]; function rebuildLines(){ lines.forEach(l=>{l.geometry.dispose(); l.material.dispose(); lineGroup.remove(l);}); lines.length=0; // 每个城市连接 2-3 个就近城市 CITIES.forEach((c,i)=>{ const others = CITIES .map((o,j)=>({j,d:Math.hypot(c.lat-o.lat,c.lng-o.lng)})) .filter(x=>x.j!==i) .sort((a,b)=>a.d-b.d) .slice(0,2+Math.floor(Math.random()*2)); others.forEach(o=>{ if(i{ const N = 2 + Math.floor(Math.random()*2); for(let i=0;i{ const div = document.createElement('div'); div.className='city-item'; div.innerHTML = `${c.n}${c.pop.toLocaleString()}`; div.onclick = ()=>focusCity(i); cityListEl.appendChild(div); }); function focusCity(i){ const city = CITIES[i]; $('#ic-name').textContent = city.n; $('#ic-country').textContent = city.c + ' · ' + city.e; $('#ic-lat').textContent = city.lat.toFixed(4) + '°'; $('#ic-lng').textContent = city.lng.toFixed(4) + '°'; $('#ic-pop').textContent = city.pop.toLocaleString(); $('#ic-tz').textContent = city.tz; $('#ic-time').textContent = getCityTime(city); $('#ic-users').textContent = (Math.floor(city.pop*0.0003+Math.random()*5000)).toLocaleString(); // 设置信息卡中的大国旗 const flagEl = $('#ic-flag'); flagEl.style.backgroundImage = `url(flags/${city.cc}.png)`; flagEl.classList.remove('wave'); void flagEl.offsetWidth; flagEl.classList.add('wave'); // 标记3D高亮旗帜 activeFlag = flagMeshes.find(f => f.userData.city && f.userData.city.n === city.n) || null; if(activeFlag){ activeFlag.userData.waveBoost = 1; activeFlag.userData.fadeIn = 0; } $('#infoCard').classList.add('show'); // 视角飞向 const p = latLngToVec3(city.lat, city.lng, R); const camPos = p.clone().multiplyScalar(3.0); animateCamera(camPos, p.clone().multiplyScalar(0)); } window.hideInfo = ()=>$('#infoCard').classList.remove('show'); function getCityTime(city){ const offset = parseFloat(city.tz.replace('UTC','').replace(':','.')||'0'); const now = new Date(); const utc = now.getTime() + now.getTimezoneOffset()*60000; const t = new Date(utc + offset*3600000); return `${fmt(t.getHours())}:${fmt(t.getMinutes())}:${fmt(t.getSeconds())}`; } let camTarget = null, camPosTarget = null, camAnim = null; function animateCamera(toPos, toTarget){ camPosTarget = toPos; camTarget = toTarget; camAnim = {t:0, from:camera.position.clone(), fromT:controls.target.clone()}; } function updateCameraAnim(dt){ if(!camAnim) return; camAnim.t += dt*0.6; if(camAnim.t >= 1){ camAnim = null; camTarget = null; camPosTarget = null; return; } const t = camAnim.t; const e = t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2; camera.position.lerpVectors(camAnim.from, camPosTarget, e); controls.target.lerpVectors(camAnim.fromT, camTarget, e); } /* 鼠标点击 */ const ray = new THREE.Raycaster(); const mouse = new THREE.Vector2(); let activeFlag = null; // 当前高亮飘动的旗帜 renderer.domElement.addEventListener('click', e=>{ if(e.target !== renderer.domElement) return; mouse.x = (e.clientX/innerWidth)*2-1; mouse.y = -(e.clientY/innerHeight)*2+1; ray.setFromCamera(mouse, camera); // 同时检测城市圆点和旗帜 const targets = cityMeshes.filter(m=> (m.geometry.type==='SphereGeometry' && m.geometry.parameters.radius<0.02) || (m.userData && m.userData.isFlag) ); const hits = ray.intersectObjects(targets); if(hits.length){ const obj = hits[0].object; const city = obj.userData.city || obj.userData; focusCity(CITIES.findIndex(c=>c.n===city.n)); } }); /* =========================================================== 控件 =========================================================== */ let autoRotate=true, showClouds=true, showStars=true, showAtm=true, showLines=true, showFlags=true, timeSpeed=1; $('#btnRotate').onclick = e=>{ autoRotate=!autoRotate; e.target.classList.toggle('active',autoRotate); }; $('#btnClouds').onclick = e=>{ showClouds=!showClouds; clouds.visible=showClouds; e.target.classList.toggle('active',showClouds); }; $('#btnStars').onclick = e=>{ showStars=!showStars; stars.visible=showStars; e.target.classList.toggle('active',showStars); }; $('#btnAtm').onclick = e=>{ showAtm=!showAtm; atmo.visible=showAtm; halo.visible=showAtm; e.target.classList.toggle('active',showAtm); }; $('#btnLines').onclick = e=>{ showLines=!showLines; lineGroup.visible=showLines; particles.forEach(p=>p.visible=showLines); e.target.classList.toggle('active',showLines); }; $('#btnFlags').onclick = e=>{ showFlags=!showFlags; flagMeshes.forEach(f=>f.visible=showFlags); e.target.classList.toggle('active',showFlags); }; const speeds=[1,60,600,3600]; let spIdx=0; $('#btnSpeed').onclick = e=>{ spIdx=(spIdx+1)%speeds.length; timeSpeed=speeds[spIdx]; e.target.textContent = `时间 ×${timeSpeed}`; e.target.classList.toggle('active', timeSpeed!==1); }; $('#btnReset').onclick = ()=>{ animateCamera(new THREE.Vector3(0,1.5,4.5), new THREE.Vector3(0,0,0)); }; // 默认激活态 ['btnRotate','btnClouds','btnStars','btnAtm','btnLines','btnFlags'].forEach(id=>$('#'+id).classList.add('active')); /* =========================================================== 动画 =========================================================== */ let simTime = Date.now(); const clock = new THREE.Clock(); let frame=0, lastFps=performance.now(), fps=0; function tick(){ const dt = clock.getDelta(); simTime += dt*1000*timeSpeed; // 自转 if(autoRotate && !camAnim){ earth.rotation.y += dt*0.05; clouds.rotation.y += dt*0.06; } if(!camAnim){ // 鼠标控制 controls.update(); } else { updateCameraAnim(dt); } // 太阳位置(时间驱动) const ang = (simTime / (86400000)) * Math.PI*2; sun.position.set(Math.cos(ang)*8, Math.sin(ang*0.5)*2, Math.sin(ang)*8); sun.intensity = Math.max(0, Math.sin(ang))*1.4 + 0.3; // 城市脉冲环 cityMeshes.forEach(m=>{ if(m.geometry.type==='RingGeometry'){ const s = 1 + Math.sin(simTime*0.003 + m.userData.pulse)*0.5; m.scale.setScalar(s); m.material.opacity = 0.3 + (1.5-s)*0.4; } }); // 国旗飘动 + 始终面向相机 flagMeshes.forEach(flag=>{ flag.material.uniforms.uTime.value = simTime*0.001; // waveBoost 衰减 if(flag.userData.waveBoost > 0){ flag.userData.waveBoost = Math.max(0, flag.userData.waveBoost - dt*0.5); } flag.material.uniforms.uWave.value = 1.0 + flag.userData.waveBoost*2.0; // 始终面向相机(billboard) flag.lookAt(camera.position); }); // 粒子沿连线流动 particles.forEach(p=>{ if(!p.visible) return; p.userData.t += dt*0.4*timeSpeed; if(p.userData.t > 1) p.userData.t -= 1; const pos = p.userData.line.geometry.attributes.position; const idx = Math.floor(p.userData.t*(pos.count-1)); p.position.fromBufferAttribute(pos, idx); }); // 星空缓慢旋转 if(stars) stars.rotation.y += dt*0.0008; renderer.render(scene, camera); // FPS frame++; if(performance.now()-lastFps>=500){ fps=Math.round(frame*1000/(performance.now()-lastFps)); $('#fps').textContent=fps; lastFps=performance.now(); frame=0; } // UTC const d = new Date(simTime); $('#utc').textContent = `${fmt(d.getUTCHours())}:${fmt(d.getUTCMinutes())}:${fmt(d.getUTCSeconds())}`; $('#online').textContent = (1250000 + Math.floor(Math.sin(simTime*0.0001)*50000)).toLocaleString(); requestAnimationFrame(tick); } /* =========================================================== 自适应 =========================================================== */ addEventListener('resize', ()=>{ camera.aspect = innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); }); /* 启动 */ setTimeout(()=>{ $('#loader').style.opacity='0'; setTimeout(()=>$('#loader').remove(), 500); tick(); }, 300);