// 3D 地球效果(首页 banner 右侧)基于 three-globe // 依赖:全局 THREE 和 ThreeGlobe(在 index.html 中通过 CDN 引入) (function () { function initGlobe() { var container = document.getElementById("bannerGlobe"); if (!container) { console.warn("[Globe] bannerGlobe container not found"); return; } if (typeof THREE === "undefined") { console.warn( "[Globe] THREE is undefined, please ensure three.min.js is loaded" ); return; } if (typeof ThreeGlobe === "undefined") { console.warn( "[Globe] ThreeGlobe is undefined, please ensure three-globe.min.js is loaded" ); return; } var width = container.clientWidth || 400; var height = container.clientHeight || 400; // 基础 Three.js 场景 var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(40, width / height, 0.1, 1000); // 初始视角,具体距离在 globe 准备好后根据半径自动调整 camera.position.set(0, 0.2, 8.0); camera.lookAt(0, 0, 0); var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); renderer.setSize(width, height); renderer.setClearColor(0x000000, 0); // 透明背景 container.appendChild(renderer.domElement); // 柔和光照 var ambient = new THREE.AmbientLight(0xffffff, 0.65); scene.add(ambient); var dirLight = new THREE.DirectionalLight(0xffffff, 0.9); dirLight.position.set(5, 3, 5); scene.add(dirLight); // 节点数据示例(可替换为真实机房经纬度,至少 15 个) var nodes = [ { name: "Beijing", lat: 39.9, lng: 116.4 }, { name: "Shanghai", lat: 31.2, lng: 121.5 }, { name: "Guangzhou", lat: 23.1, lng: 113.3 }, { name: "Shenzhen", lat: 22.5, lng: 114.1 }, { name: "Hong Kong", lat: 22.3, lng: 114.2 }, { name: "Singapore", lat: 1.3, lng: 103.8 }, { name: "Tokyo", lat: 35.7, lng: 139.7 }, { name: "Osaka", lat: 34.7, lng: 135.5 }, { name: "Seoul", lat: 37.5, lng: 126.9 }, { name: "Sydney", lat: -33.9, lng: 151.2 }, { name: "Mumbai", lat: 19.0, lng: 72.8 }, { name: "Frankfurt", lat: 50.1, lng: 8.7 }, { name: "London", lat: 51.5, lng: -0.1 }, { name: "Paris", lat: 48.9, lng: 2.4 }, { name: "New York", lat: 40.7, lng: -74.0 }, { name: "San Francisco", lat: 37.8, lng: -122.4 } ]; // 基于节点构造“飞行轨迹”连接数据(startLat/startLng -> endLat/endLng) function buildArcsFromNodes(list) { var arcs = []; if (!Array.isArray(list) || list.length < 2) { return arcs; } // 简单策略:每个节点连接到后面第 step 个节点,形成环状主干网络 var step = Math.max(1, Math.floor(list.length / 3)); list.forEach(function (src, idx) { var dst = list[(idx + step) % list.length]; if (!dst || (dst.lat === src.lat && dst.lng === src.lng)) { return; } arcs.push({ startLat: src.lat, startLng: src.lng, endLat: dst.lat, endLng: dst.lng, // 统一使用偏青色的轨迹,贴合云服务器 / CDN 的科技感 color: "#38BDF8", }); }); return arcs; } // three-globe 实例:使用空心陆地多边形(hollow globe 风格) var globe = new ThreeGlobe({ waitForGlobeReady: true, animateIn: true, }) // 不使用 three-globe 内置球体贴图,只保留大气层,陆地轮廓由 polygons 层绘制 .showGlobe(false) .showAtmosphere(false) .atmosphereColor("#2b9fff") .atmosphereAltitude(0.18) .showGraticules(false); scene.add(globe); // 构造并应用“节点连接 + 飞行轨迹”效果 var arcs = buildArcsFromNodes(nodes); if (typeof globe.arcsData === "function" && arcs.length) { globe.arcsData(arcs); if (typeof globe.arcColor === "function") { globe.arcColor(function (d) { return d.color || "#38BDF8"; }); } if (typeof globe.arcAltitude === "function") { globe.arcAltitude(0.2); } if (typeof globe.arcStroke === "function") { globe.arcStroke(0.7); } // 使用虚线 + 动画时间制造“飞行”感 if (typeof globe.arcDashLength === "function") { globe.arcDashLength(0.35); } if (typeof globe.arcDashGap === "function") { globe.arcDashGap(0.8); } if (typeof globe.arcDashInitialGap === "function") { globe.arcDashInitialGap(function () { return Math.random(); }); } if (typeof globe.arcDashAnimateTime === "function") { globe.arcDashAnimateTime(function () { // 4~7 秒一圈,避免所有轨迹节奏完全一致 return 4000 + Math.random() * 3000; }); } } // 告诉 three-globe 当前渲染器的实际尺寸,避免默认按全窗口大小计算导致比例失真 if (typeof globe.rendererSize === "function") { globe.rendererSize(new THREE.Vector2(width, height)); } // 在 globe 初始化完成后,根据实际半径自动调整相机距离,保证整球落在视野内 if (typeof globe.onGlobeReady === "function" && typeof globe.getGlobeRadius === "function") { globe.onGlobeReady(function () { var r = globe.getGlobeRadius(); // three-globe 内部使用的球半径 // 经验:以半径的约 3 倍距离拍摄,配合 40° 视角,可以完整显示球体并留一定留白 var dist = r * 3.2; camera.position.set(0, 0.2, dist); camera.lookAt(0, 0, 0); // 在陆地多边形下方放置一个略小的球体作为“海洋”,用白色突出球体轮廓 // 颜色和透明度可以在这里调整 var oceanGeom = new THREE.SphereGeometry(r * 0.99, 64, 64); var oceanMat = new THREE.MeshPhongMaterial({ color: "#FFFFFF", // 海洋/球体底色:白色(可改成其他颜色) transparent: true, opacity: 1, // 透明度:越接近 1 越实 shininess: 40, side: THREE.FrontSide, }); var oceanMesh = new THREE.Mesh(oceanGeom, oceanMat); globe.add(oceanMesh); // 在节点位置放置小型静态“节点点” var nodeGeom = new THREE.SphereGeometry(r * 0.02, 16, 16); var nodeMat = new THREE.MeshBasicMaterial({ color: "#38BDF8", transparent: true, opacity: 0.9, }); // 通知 three-globe 当前视角,用于部分图层的内部计算 if (typeof globe.setPointOfView === "function") { globe.setPointOfView(camera); } // 使用 globe 的工具方法,将经纬度转换为球面上的 3D 坐标 var hasGetCoords = typeof globe.getCoords === "function"; nodes.forEach(function (n) { var pos; if (hasGetCoords) { // altitude 略高于球面,避免被多边形遮挡 pos = globe.getCoords(n.lat, n.lng, 0.02); } else { // 兼容降级:简单根据半径和经纬度计算 var phi = (90 - n.lat) * (Math.PI / 180); var theta = (n.lng + 180) * (Math.PI / 180); var rr = r * 1.02; pos = { x: -rr * Math.sin(phi) * Math.cos(theta), z: rr * Math.sin(phi) * Math.sin(theta), y: rr * Math.cos(phi) }; } var nodeMesh = new THREE.Mesh(nodeGeom, nodeMat); nodeMesh.position.set(pos.x, pos.y, pos.z); globe.add(nodeMesh); }); }); } // 按 hollow-globe 风格加载陆地多边形,绘制空心地球轮廓 (function loadLandPolygons() { if (typeof topojson === "undefined") { console.warn( "[Globe] topojson-client is undefined, unable to render hollow globe polygons" ); return; } // 请确保 /web/BlackFruit-web/assets/data/land-110m.json 存在, // 内容为你刚才提供的 Topology JSON。 fetch("/web/BlackFruit-web/assets/data/land-110m.json") .then(function (res) { return res.json(); }) .then(function (landTopo) { try { var landGeo = topojson.feature( landTopo, landTopo.objects.land ).features; globe .polygonsData(landGeo) .polygonCapMaterial( new THREE.MeshLambertMaterial({ // 更亮、更偏青的科技蓝陆地,与轨迹/节点颜色统一 color: "#38BDF8", transparent: true, opacity: 0.75, side: THREE.DoubleSide, }) ) .polygonSideColor(function () { return "rgba(0,0,0,0)"; }); } catch (e) { console.warn("[Globe] failed to parse land-110m topology:", e); } }) .catch(function (err) { console.warn( "[Globe] failed to load /web/BlackFruit-web/assets/data/land-110m.json:", err ); }); })(); // 暴露给调试用 if (typeof window !== "undefined") { window.__globe = globe; } // 自适应窗口大小 function onResize() { if (!container) return; var w = container.clientWidth || width; var h = container.clientHeight || height; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); // 同步 three-globe 对 renderer 尺寸的感知,保证缩放比例一致 if (typeof globe.rendererSize === "function") { globe.rendererSize(new THREE.Vector2(w, h)); } } window.addEventListener("resize", onResize); // 动画循环 var lastTime = performance.now(); function animate() { requestAnimationFrame(animate); var now = performance.now(); var delta = (now - lastTime) / 1000; lastTime = now; // 地球自转 globe.rotation.y += delta * 0.25; renderer.render(scene, camera); } animate(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initGlobe); } else { initGlobe(); } })();