新闻详情

新闻详情

首页 / 资讯中心 / 详情

Three.js 3D地球可视化实战:纹理、交互与性能优化全解析

发布时间:2026/9/29 2:30:44来源:尧图网络
Three.js 3D地球可视化实战:纹理、交互与性能优化全解析
第一次正儿八经用 Three.js 做 3D 地球可视化的时候我交出的作品堪称灾难一个灰扑扑的球体表面光滑得毫无细节转起来像一个没贴包装纸的篮球跟“地球”两个字沾不上半点边。后来我把地图贴图换上去又发现大陆轮廓怎么都对不齐拖动视角时卡顿明显在移动端上更是直接卡成 PPT。做 3D 地球可视化难点从来不在“怎么画一个球”而在于球体之外那一整套工程细节——纹理怎么贴合、经纬度怎么换算成坐标、交互怎么处理、性能怎么兜底。这篇内容就是我踩完这些坑之后整理出的完整方案基于 Three.js 最新的模块化版本从零开始搭建一个具备纹理贴图、自转动画、鼠标拖拽旋转、大气光晕、地点标记的 3D 地球你可以直接照搬落地。无论你是刚接触 Three.js 的前端还是需要在三维球体上做数据展示的开发者这篇实战笔记应该都能帮你省掉不少折腾时间。1. 先把“球”渲染出来从空场景到带纹理的地球1.1 三维场景的最小骨架不能省网上很多教程喜欢从 CDN 引一个老旧的 three.min.js然后塞给你一大坨全局变量。这种写法在本地 Demo 里跑着没问题但放到真实项目里你要做的是构建工具、模块化引入、按需打包。所以我这里直接采用 npm 安装、ES Module 引入的最新版写法这也是当前 Three.js 官方主推的方式。npm install three安装完成后一个最小可渲染的三维场景包含四样东西渲染器、场景、相机、动画循环。对应到 Three.js 里就是WebGLRenderer、Scene、PerspectiveCamera外加一个用requestAnimationFrame驱动的 render loop。import * as THREE from three; import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 0, 10); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate();有几点想提醒你注意。renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))这条必须写尤其是移动端如果不限制像素比高分屏上 WebGL 会按 3x、4x 去渲染性能白白浪费一大截。antialias: true是抗锯齿开关开着能让球体边缘平滑很多代价是少量性能损耗做地球这个场景完全值得。1.2 球体网格与纹理贴图一切从一张图开始有了场景骨架接下来往里面放球体。Three.js 提供了SphereGeometry基础用法很简单const sphereGeometry new THREE.SphereGeometry(3, 64, 64); const sphereMaterial new THREE.MeshPhongMaterial({ map: new THREE.TextureLoader().load(/textures/earth.jpg), }); const earth new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(earth);这里有两个细节很多人会踩坑。第一个是球体分段数。SphereGeometry(radius, widthSegments, heightSegments)的第二、三个参数是经线和纬线的分段数分段数越高球面越平滑。数值设置太低球体边缘会出现明显的棱角太高了顶点的数量会暴涨影响性能。做地球可视化半径 3 左右的话 64x64 完全够用肉眼已经分辨不出多边形的边了。第二个细节是纹理的颜色空间。Three.js 从 r152 版本开始默认把纹理当作线性色彩空间处理如果你不做任何设置贴图看起来会偏灰偏淡。正确做法是给纹理打上SRGBColorSpace标记const textureLoader new THREE.TextureLoader(); const earthTexture textureLoader.load(/textures/earth.jpg); earthTexture.colorSpace THREE.SRGBColorSpace;这个坑特别隐蔽因为旧版本不会出现这个问题很多老教程也没提升级版本之后你会发现界面颜色整体不对劲但又不是完全错乱排查起来很花时间。1.3 为什么地球“睡倒”了坐标轴与贴图对齐我第一次把带纹理的球放进场景发现它像一个被放倒的地球仪北极指向 Y 轴正方向也就是朝向屏幕上方但现实中的地球自转轴是接近垂直于黄道面的这个状态看着特别别扭。原因在于 Three.js 的坐标系约定与地图纹理的 UV 映射方式。球体几何体的极点默认在 Y 轴上而等距柱状投影的地图纹理其 UV 坐标在球面上的分布规律是“赤道在中间、南北极在上下两端”。为了让贴图看起来符合直觉要么把纹理在 Photoshop 里旋转要么直接把球体在 X 轴上旋转 90 度。更优雅的办法是创建一个组把地球放在组里然后让组带着地轴倾角去倾斜const earthGroup new THREE.Group(); earthGroup.rotation.z 23.5 * Math.PI / 180; earthGroup.add(earth); scene.add(earthGroup);地轴倾角 23.5 度是真实地球的黄赤交角加进去之后整个地球自然很多后续做公转、昼夜模拟时也更真实。2. 让地球“活”过来自转、地轴与星空背景2.1 自转动画的正确驱动方式地球静止在那里是没有灵魂的自转是第一个必须加的动画。 Three.js 的动画循环里有一个关键概念帧率无关的动画。如果你直接写成earth.rotation.y 0.01;在 60Hz 刷新率的屏幕上看着正常换到 120Hz 的显示器上地球转速直接快一倍。正确做法是使用THREE.Clock获得每帧间隔时间delta然后乘以一个固定速度系数const clock new THREE.Clock(); function animate() { const delta clock.getDelta(); earth.rotation.y 0.05 * delta; renderer.render(scene, camera); requestAnimationFrame(animate); } animate();这里0.05表示每秒大约旋转 0.05 弧度换算一下大概每 125 秒转一圈是一个看起来优雅又不会让人头晕的速度。实际项目里你可以根据展示需求调整想快一点就加大系数值。2.2 自转轴、旋转中心与组的层级关系自转动画加在哪一层是初学者最容易搞混的。上面我把地球放到了earthGroup里组本身带了 23.5 度的倾角。现在要让地球绕自身轴旋转旋转应该加在earth网格上而不是earthGroup上。这样地球会带着倾角一起转姿态始终符合地轴倾角的视觉效果。如果反过来你把旋转加在earthGroup上效果就变成了整个地轴线在摆动地球像一个不倒翁一样晃来晃去完全不是自转效果。这是层级关系没理清造成的。我在实际项目中见过不少这种用法错误排查了半天才发现是旋转对象搞错。同时还要注意场景中如果还有其他对象比如卫星、轨道线它们的父子关系会直接影响运动表现。凡是跟地球固定在一起的放earthGroup下凡是独立运动的放场景根节点下。2.3 星空背景粒子系统比背景贴图更香背景最简单的做法是给场景一个纯色scene.background new THREE.Color(0x000011);但纯黑背景很单调地球可视化项目通常需要星空点缀。有两个方案一是用立方纹理贴图CubeTexture需要准备六张星空图二是用粒子系统Points做随机星空。我推荐粒子系统因为它体积小、加载快而且做出来的星空有纵深感和闪烁效果观感远好于静态贴图。function createStars() { const vertices []; for (let i 0; i 5000; i) { const x (Math.random() - 0.5) * 2000; const y (Math.random() - 0.5) * 2000; const z (Math.random() - 0.5) * 2000; vertices.push(x, y, z); } const geometry new THREE.BufferGeometry(); geometry.setAttribute(position, new THREE.Float32BufferAttribute(vertices, 3)); const material new THREE.PointsMaterial({ color: 0xffffff, size: 0.8, sizeAttenuation: true, }); return new THREE.Points(geometry, material); } scene.add(createStars());需要注意星空的坐标范围要足够大确保相机无论怎么拖动都穿不过星空层。我用的 2000 范围配合相机距离默认 10是足够用的。sizeAttenuation: true让星星离得远时看起来更小有纵深层次感比平面贴图效果好很多。3. 交互是可视化的灵魂鼠标拖拽、缩放与触控适配3.1 OrbitControls 引入与基础配置地球可视化如果只能傻转那价值大打折扣。用户需要拖拽旋转视角、滚动缩放远近这是OrbitControls存在的原因。它本质上是把你对鼠标/触控板/触摸屏的输入事件翻译成相机的轨道运动——相机始终围绕一个目标点旋转这正是观察地球所需要的。const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.dampingFactor 0.08; controls.minDistance 5; controls.maxDistance 20; controls.enablePan false; controls.autoRotate false;这里有几个参数值得单独说明。第一enableDamping是惯性效果开关开启后拖动地球松手时地球会带着一点点惯性继续滑动手感从“生硬”变成“丝滑”。这是所有三维可视化项目里提升质感最重要的一行代码。第二minDistance和maxDistance限制了相机缩放范围。对于 3 半径的地球min 设为 5 能防止相机钻到球体内部去max 设为 20 能避免拉太远导致地球在屏幕上变成一个点。具体数值取决于你地球半径的设定。第三enablePan false禁用了平移。因为地球场景不需要用户把地球拖到屏幕外面去开着平移容易造成视角迷失。3.2 阻尼系数与旋转速率的调校心得dampingFactor这个值的调校非常影响手感。默认 0.05 其实偏慢我习惯调到 0.08~0.12 之间。数值越大惯性越明显但太大比如超过 0.25会感觉视角“黏糊糊”的拖过去半天停不下来。这是一个纯主观的手感参数建议你按自己的操作习惯多试几个值找到那个“指哪打哪、又带一点余韵”的感觉。另外OrbitControls 默认支持右键拖拽平移。因为我们禁用了enablePan右键操作会被忽略有些用户可能习惯右键平移这个可以根据自己的需求决定是否启用。3.3 移动端触控适配一场不能输的仗现在很多三维可视化项目超过一半的流量来自手机端如果你的页面在手机上没法用等于放弃了一半用户。Three.js 在移动端适配方面有一些必须处理的细节。首先是视口与缩放function onResize() { camera.aspect window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener(resize, onResize);这是所有 Three.js 项目都必须处理的少写这段页面一缩放就会变形。其次是触控手势。好消息是 OrbitControls 内部已经实现了触摸事件的处理单指拖拽旋转、双指捏合缩放。你不需要额外写任何触控代码。但需要注意一个问题如果页面本身可以滚动用户在操作地球时会把页面一起带滚。解决办法是给地球所在的 canvas 容器加上阻止默认滚动的手势锁canvas { display: block; touch-action: none; }touch-action: none表示禁止浏览器处理触摸手势的默认行为把事件全部交给 WebGL 画布里的 OrbitControls 来处理。这行 CSS 忘记加移动端会出现页面跟着地球一起滚动的体验非常糟糕。4. 进阶表现力大气光晕、地点标记与信息弹窗4.1 大气光晕用 Shader 做一层边缘发光一个光秃秃的地球贴图虽然能看但缺少“星球”的质感。大气光晕是让地球真正有星球感的关键效果——在球体外围添加一圈从蓝到透明的发光层视觉上就像地球的大气层被太阳照亮。实现思路很经典再创建一个比地球略大的球体材质使用自定义的ShaderMaterial通过视角方向与法线方向的夹角来控制透明度——边缘夹角小光晕强中心夹角大透明。const atmosphereMaterial new THREE.ShaderMaterial({ vertexShader: varying vec3 vNormal; void main() { vNormal normalize(normalMatrix * normal); gl_Position projectionMatrix * modelViewMatrix * vec4(position, 1.0); } , fragmentShader: varying vec3 vNormal; void main() { float intensity pow(0.7 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0); gl_FragColor vec4(0.3, 0.6, 1.0, 1.0) * intensity; } , blending: THREE.AdditiveBlending, side: THREE.BackSide, transparent: true, }); const atmosphere new THREE.Mesh( new THREE.SphereGeometry(3.15, 64, 64), atmosphereMaterial ); earthGroup.add(atmosphere);这段 Shader 的关键在于dot(vNormal, vec3(0.0, 0.0, 1.0))计算的是法线与视线方向夹角的余弦值。球体边缘处法线几乎垂直视线余弦值接近 00.7 - 0得到较大的值再通过pow(x, 2.0)强化衰减形成边缘亮、中心暗的菲涅尔效果。side: THREE.BackSide表示只渲染球体内表面配合AdditiveBlending叠加混合模式光晕会自然叠加在地球背后。我最初做这一步时长长调不出效果后来发现是材质transparent没设成 true。ShaderMaterial 不会自动开启透明必须手动设置。4.2 经纬度坐标换算把数据点放到地球的正确位置大部分地球可视化项目都需要在地球表面标记地点。这里最核心的一个函数是经纬度转三维坐标。地球表面上的任意一点可以由经纬度唯一确定换算成三维笛卡尔坐标的公式如下function latLonToVector3(lat, lon, radius) { const phi (90 - lat) * Math.PI / 180; const theta (lon 180) * Math.PI / 180; return new THREE.Vector3( -radius * Math.sin(phi) * Math.cos(theta), radius * Math.cos(phi), radius * Math.sin(phi) * Math.sin(theta) ); }这里有三个容易出错的细节。第一radius参数必须比地球半径稍大一点比如地球半径是 3标记点半径就用3.01否则标记点会嵌入球体表面被地球本体遮挡。第二公式里的phi从北极开始计算所以用90 - lattheta从本初子午线开始所以用lon 180能保证标记点和纹理贴图上的地理位置严格对齐。第三X 轴的负号是有意为之这是为了匹配 Three.js 右手坐标系下的贴图方向。我第一次写这段时没加负号结果所有地点都跑到了对称的镜像位置上。有了坐标添加标记就有很多方案可选。最简单的是用THREE.Spritefunction createMarker(lat, lon) { const position latLonToVector3(lat, lon, 3.01); const canvas document.createElement(canvas); canvas.width 64; canvas.height 64; const ctx canvas.getContext(2d); ctx.beginPath(); ctx.arc(32, 32, 16, 0, Math.PI * 2); ctx.fillStyle #ff4d4f; ctx.fill(); ctx.strokeStyle #ffffff; ctx.lineWidth 4; ctx.stroke(); const texture new THREE.CanvasTexture(canvas); texture.colorSpace THREE.SRGBColorSpace; const material new THREE.SpriteMaterial({ map: texture, depthTest: false }); const sprite new THREE.Sprite(material); sprite.position.copy(position); sprite.scale.set(0.3, 0.3, 1); return sprite; }Canvas 绘制标记点的好处是灵活你可以画圆点、画定位图标、画数据气泡生成纹理后交给 Sprite 贴图不需要额外的模型文件。depthTest: false可以让标记点始终显示在最前面不受球体遮挡。这个取舍要看场景——如果标记点数量很多我建议还是开 depthTest 并配合偏移否则标记点会穿透地球观感错乱。4.3 让标记点可点击射线拾取与信息面板标记放上去了下一步自然是点击交互——点击某个城市标记弹出对应的信息面板。 Three.js 中做点击拾取的标准方案是Raycaster从相机位置发射一条射线穿过鼠标点击位置检测射线在场景中与哪些物体相交。const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); renderer.domElement.addEventListener(click, (event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(markers); if (intersects.length 0) { const marker intersects[0].object; showInfoPanel(marker.userData); } });Raycaster的原理可以理解成“从相机出发沿着鼠标指向的方向射出一条不可见的光线看看沿途碰到哪些东西”。mouse.x和mouse.y需要把屏幕坐标归一化到 [-1, 1] 区间这个换算很容易写错坐标算错就导致怎么点都选不中。每个标记点的信息可以通过userData挂载sprite.userData { name: 北京, lat: 39.9042, lon: 116.4074, description: 中国的首都, url: https://example.com/beijing, };点击后再用 DOM 创建信息面板。三维世界里做 UI 有个选择用 HTML 覆盖层绝对定位的 div还是用 CSS2DRenderer 把 HTML 元素绑定到三维坐标上。如果只是点击后弹一个信息面板HTML 覆盖层最简单可靠如果你需要很多标签常驻在三维场景里跟随旋转CSS2DRenderer 是更好的选择。5. 性能优化与生产部署从能跑到跑得稳5.1 几何体精度与顶点数量的取舍如果一个 3D 地球项目在桌面端很流畅但在手机上卡顿首要检查的就是几何体分段数。SphereGeometry(3, 64, 64)会产生(641) * (641) 4225个顶点这其实已经相当高了。做移动端适配时我会把分段数降到32, 32顶点数量降到1089个球体边缘和贴图效果肉眼看几乎没有差别但 GPU 负载降低了一截。值得强调的是球体本身不是性能瓶颈贴图尺寸往往才是。一张 4096x4096 的地球纹理解码后占用的 GPU 内存大约是4096 * 4096 * 4 64MB如果贴图尺寸翻到 8192内存直接变 256MB。移动端 GPU 内存本来就不宽裕我建议地球纹理最多用 2048x2048这个分辨率在 3 半径的球体上完全够清晰。可以通过在线工具或者 Photoshop 提前压缩纹理尺寸肉眼几乎分辨不出差别。5.2 纹理加载策略不要阻塞主渲染TextureLoader().load()默认是异步加载纹理没有加载完成时场景里会出现一个无贴图的白模体验不好。我建议在加载纹理时显式处理加载状态const textureLoader new THREE.TextureLoader(); function loadTexture(url) { return new Promise((resolve) { textureLoader.load(url, (texture) { texture.colorSpace THREE.SRGBColorSpace; resolve(texture); }); }); } async function init() { const earthTexture await loadTexture(/textures/earth.jpg); const material new THREE.MeshPhongMaterial({ map: earthTexture }); // 继续构建场景 }这样你在渲染之前就知道纹理已经就位可以避免白模闪现。项目大一点可以把纹理加载、模型加载都包成一个 Promise全部加载完再显示页面。配合一个简单的 loading 动画用户感知到的页面质量会大大提升。这里顺便推荐几个免费的地球纹理源Solar System Scope 的纹理贴图有日间、夜间、法线贴图、NASA Blue Marble 系列、以及 Three.js 官方示例仓库里的地球纹理。如果你想做夜景灯光效果可以用夜间灯光贴图替换日间贴图再叠加一个MeshBasicMaterial效果会很惊艳。5.3 常见问题排查清单做 3D 地球可视化最容易遇到的几个问题我把排查思路整理成一个清单你照着查基本可以解决 90% 的问题现象可能原因解决方案地球是黑色的忘记加灯光添加DirectionalLight和AmbientLight贴图显示偏灰偏暗纹理颜色空间错误texture.colorSpace THREE.SRGBColorSpace经度/纬度标记位置错乱经纬度换算公式不对对照公式检查phi、theta计算标记点被球体遮挡radius 偏移不足标记点的 radius 设置为地球半径 0.01拖动页面也跟着滚动缺少touch-action: nonecanvas 样式加上touch-action: none画面模糊或锯齿明显缺少抗锯齿或像素比限制antialias: true限制pixelRatio缩放时相机穿进地球minDistance 设置过小设置minDistance大于地球半径点击标记点没反应屏幕坐标归一化错误检查mouse.x/mouse.y的换算表格里列的问题每个我都真实遇到过。其中“地球是黑色”最坑Three.js 的MeshPhongMaterial依赖光线计算你不加灯光物体就是全黑的很多初学者会误以为是贴图加载失败折腾半天最后发现只是少了new THREE.DirectionalLight()这一行。5.4 从单页 Demo 到生产项目工程化建议如果要把地球可视化放进实际项目还有几个工程层面的建议。第一把渲染器、场景、相机、初始化流程封装成一个EarthVisualization类仅对外暴露init(container)、addMarker(data)、destroy()这几个方法。这样既方便在其他页面复用也方便销毁时清理 WebGL 资源。第二注意内存释放。页面切换或组件卸载时要调用renderer.dispose()、material.dispose()、geometry.dispose()否则 WebGL 上下文可能堆积最终导致浏览器崩溃。我自己踩过这个坑单页应用里频繁切换路由内存占用一路飙升排查了很久才发现是旧场景没有释放。第三考虑 WebGL 上下文丢失的情况。移动端浏览器在后台标签页被系统回收时WebGL 上下文可能丢失回到页面时画面会变成白板。监听webglcontextlost和webglcontextrestored事件做相应的处理renderer.domElement.addEventListener(webglcontextlost, (event) { event.preventDefault(); }); renderer.domElement.addEventListener(webglcontextrestored, () { init(); // 重新初始化场景 });6. 完整演示一个可以直接运行的入门版本把上面所有内容串起来我整理了一个可以直接运行的 HTML 页面。这个版本包含基础地球、自转、星空、大气光晕和三个标记点所有资源都引用的 CDN 纹理地址你可以直接复制保存为.html文件运行。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleThree.js 3D地球可视化/title style * { margin: 0; padding: 0; } body { background: #000; overflow: hidden; } canvas { display: block; touch-action: none; } #info { position: absolute; left: 20px; bottom: 20px; color: rgba(255, 255, 255, 0.6); font-family: system-ui, sans-serif; font-size: 14px; pointer-events: none; } /style /head body div idinfo鼠标拖拽旋转 | 滚轮缩放 | 点击标记查看详情/div script typeimportmap { imports: { three: https://unpkg.com/three0.160.0/build/three.module.js, three/addons/: https://unpkg.com/three0.160.0/examples/jsm/ } } /script script typemodule import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; // 场景、相机、渲染器 const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 1, 8); const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); // 灯光 const ambientLight new THREE.AmbientLight(0x404040, 2); scene.add(ambientLight); const directionalLight new THREE.DirectionalLight(0xffffff, 3); directionalLight.position.set(5, 3, 5); scene.add(directionalLight); // 地球 const earthGroup new THREE.Group(); earthGroup.rotation.z 23.5 * Math.PI / 180; scene.add(earthGroup); const textureLoader new THREE.TextureLoader(); const earthTexture textureLoader.load( https://unpkg.com/three-globe2.31.1/example/img/earth-blue-marble.jpg ); earthTexture.colorSpace THREE.SRGBColorSpace; const earth new THREE.Mesh( new THREE.SphereGeometry(3, 64, 64), new THREE.MeshPhongMaterial({ map: earthTexture }) ); earthGroup.add(earth); // 大气光晕 const atmosphereMaterial new THREE.ShaderMaterial({ vertexShader: varying vec3 vNormal; void main() { vNormal normalize(normalMatrix * normal); gl_Position projectionMatrix * modelViewMatrix * vec4(position, 1.0); } , fragmentShader: varying vec3 vNormal; void main() { float intensity pow(0.7 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0); gl_FragColor vec4(0.3, 0.6, 1.0, 1.0) * intensity; } , blending: THREE.AdditiveBlending, side: THREE.BackSide, transparent: true, }); const atmosphere new THREE.Mesh( new THREE.SphereGeometry(3.15, 64, 64), atmosphereMaterial ); earthGroup.add(atmosphere); // 星空 function createStars() { const vertices []; for (let i 0; i 6000; i) { vertices.push( (Math.random() - 0.5) * 2000, (Math.random() - 0.5) * 2000, (Math.random() - 0.5) * 2000 ); } const geometry new THREE.BufferGeometry(); geometry.setAttribute(position, new THREE.Float32BufferAttribute(vertices, 3)); const material new THREE.PointsMaterial({ color: 0xffffff, size: 0.6 }); return new THREE.Points(geometry, material); } scene.add(createStars()); // 经纬度转三维坐标 function latLonToVector3(lat, lon, radius) { const phi (90 - lat) * Math.PI / 180; const theta (lon 180) * Math.PI / 180; return new THREE.Vector3( -radius * Math.sin(phi) * Math.cos(theta), radius * Math.cos(phi), radius * Math.sin(phi) * Math.sin(theta) ); } // 创建标记点 function createMarker(lat, lon, color #ff4d4f) { const position latLonToVector3(lat, lon, 3.01); const canvas document.createElement(canvas); canvas.width 64; canvas.height 64; const ctx canvas.getContext(2d); ctx.beginPath(); ctx.arc(32, 32, 16, 0, Math.PI * 2); ctx.fillStyle color; ctx.fill(); ctx.strokeStyle #ffffff; ctx.lineWidth 4; ctx.stroke(); const texture new THREE.CanvasTexture(canvas); texture.colorSpace THREE.SRGBColorSpace; const material new THREE.SpriteMaterial({ map: texture, depthTest: false }); const sprite new THREE.Sprite(material); sprite.position.copy(position); sprite.scale.set(0.3, 0.3, 1); return sprite; } const markers []; const beijing createMarker(39.9, 116.4, #ff4d4f); beijing.userData { name: 北京, lat: 39.9, lon: 116.4, description: 中国首都 }; markers.push(beijing); const newyork createMarker(40.7, -74.0, #1890ff); newyork.userData { name: 纽约, lat: 40.7, lon: -74.0, description: 美国东海岸 }; markers.push(newyork); const london createMarker(51.5, -0.1, #52c41a); london.userData { name: 伦敦, lat: 51.5, lon: -0.1, description: 英国首都 }; markers.push(london); markers.forEach(marker earthGroup.add(marker)); // 控制器 const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; controls.dampingFactor 0.08; controls.minDistance 5; controls.maxDistance 20; controls.enablePan false; // 射线拾取 const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); function showInfoPanel(data) { // 可以替换成任何前端弹窗方案 alert(${data.name}\n${data.description}); } renderer.domElement.addEventListener(click, (event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(markers); if (intersects.length 0) { showInfoPanel(intersects[0].object.userData); } }); // 窗口自适应 window.addEventListener(resize, () { camera.aspect window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); // 动画循环 const clock new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta clock.getDelta(); earth.rotation.y 0.05 * delta; controls.update(); renderer.render(scene, camera); } animate(); /script /body /html运行这个页面你会看到一个贴有蓝色星球纹理的地球带着 23.5 度倾角缓缓自转周围有大气光晕和璀璨星空鼠标拖拽可以旋转视角滚轮可以缩放点击三个城市标记会弹出信息。这就是一个完整的地球可视化底座后续你想加航线、散点图、柱状图、实时数据绑定都是在这个骨架上做加法。我在实际项目中在这个底座上叠加过航班航线动画、全球实时气象数据、传感器分布热力图。核心思路都是一样把数据转成经纬度经纬度转成三维坐标再用 Three.js 的各类几何体去表达。所以把这篇文章里的基础打好剩下的扩展就是一件水到渠成的事。最后补充一点个人建议Three.js 的学习曲线确实有点陡峭但地球可视化是一个特别适合入门的练手项目因为它把着色器、几何体、纹理、交互、性能优化这些核心概念都覆盖了一遍做一次等于把这些知识点全部打通了。如果你在实现过程中遇到问题推荐优先去看 Three.js 官方示例中带 “webgl_animation_*” 前缀的代码很多交互细节都能在那里找到参考答案。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Windows 一键部署 OpenClaw 教程|5 分钟搞定本地 AI 智能体,TaoToken 统一 Key 接入告别复杂配置 2026/9/29 3:27:24

Windows 一键部署 OpenClaw 教程|5 分钟搞定本地 AI 智能体,TaoToken 统一 Key 接入告别复杂配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Python 操作 Oracle 数据库查询实战:cx_Oracle 配置与验证 2026/9/29 3:27:24

Python 操作 Oracle 数据库查询实战:cx_Oracle 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Woodpecker Go SDK 使用指南:用 woodpecker-go 在 Go 程序中调用 Woodpecker CI API 2026/9/29 3:27:11

Woodpecker Go SDK 使用指南:用 woodpecker-go 在 Go 程序中调用 Woodpecker CI API

CI/CDDevOps 【免费下载链接】woodpecker Woodpecker is a simple, yet powerful CI/CD engine with great extensibility. 项目地址: https://gitcode.com/gh_mirrors/wo/woodpecker 点击查看 免费下载 Woodpecker 是一个简单而强大的 CI/CD 引擎,其官…

阅读更多 →
Axure 9.0日期函数分类(系统时间函数) 2026/9/29 3:27:11

Axure 9.0日期函数分类(系统时间函数)

函数名称功能描述使用示例[[Now.getFullYear()]]获取当前年份(4位数字)[[Now.getFullYear()]] → 返回2025[[Now.getMonth()1]]获取当前月份(返回0-11,需1转换为1-12格式)[[Now.getMonth()1]] → 返回8(8月…

阅读更多 →
TFLM_day2 2026/9/29 3:27:11

TFLM_day2

轻松学习 TFLM Day 2:推理调用链图 摘要:本文是「轻松学习 TFLM」系列 Day 2,聚焦 TFLM(TensorFlow Lite for Microcontrollers)的推理调用链。文章先用一张总览图讲清 .tflite 模型从应用代码到 kernel 的完整路径,再通过一个 STM32 上运行 MNIST 手写数字识别的端到端实…

阅读更多 →
Windows 驱动实例分析系列:libwdi 驱动分析 - examples 篇(二) 2026/9/29 3:27:11

Windows 驱动实例分析系列:libwdi 驱动分析 - examples 篇(二)

子文档二:wdi-simple —— 命令行极简安装器 wdi-simple.c 是 libwdi 提供的最简单的驱动安装示例,其代码行数不足 200 行,但却完整地展示了 libwdi 的核心工作流程。它非常适合开发者快速理解如何在自己的应用程序中集成驱动安装功能。 核心…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉