Globe
Runnable three.js example: Globe. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
说明:用程序生成的等经纬度纹理包住球体,按经纬度在球面放置城市标记
核心逻辑:经纬度换算成球面坐标,CanvasTexture 贴到 SphereGeometry 上
依赖:three.js 0.160.0、OrbitControls · jsDelivr CDN(ESM + importmap)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>地球球体</title>
<style>
html,body{height:100%;margin:0;background:#0d1117}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#e6e8ec}
.bar{flex:0 0 auto;padding:8px 12px;background:#161b22;border-bottom:1px solid #252c36;
display:flex;gap:10px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
.bar b{color:#58a6ff;font-weight:600}
.bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:5px;cursor:pointer;
border:1px solid #303845;background:#1d2531;color:#c8d2e0}
.bar button:hover{background:#253040}
.bar .sp{flex:1 1 auto}
#stage{flex:1 1 auto;min-height:0;position:relative}
#stage canvas{display:block}
</style>
</head>
<body>
<div class="bar">
<button id="spin">停止自转</button>
<button id="marks">隐藏标记</button>
<span class="sp"></span>
<span>北京 116.39°E 39.91°N · 上海 121.47°E 31.23°N · 广州 113.26°E 23.13°N</span>
</div>
<div id="stage"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const stage = document.getElementById('stage');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x070b10);
const camera = new THREE.PerspectiveCamera(
42, stage.clientWidth / stage.clientHeight, 0.05, 100
);
camera.position.set(0, 1.5, 4.6);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.07;
controls.minDistance = 2.4;
controls.maxDistance = 10;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.45;
controls.update();
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
const sun = new THREE.DirectionalLight(0xfff4e0, 2.2);
sun.position.set(5, 3, 5);
scene.add(sun);
/* ---------- 程序生成等经纬度纹理 ---------- */
const TEX_W = 1024;
const TEX_H = 512;
const canvas = document.createElement('canvas');
canvas.width = TEX_W;
canvas.height = TEX_H;
const ctx = canvas.getContext('2d');
const ocean = ctx.createLinearGradient(0, 0, 0, TEX_H);
ocean.addColorStop(0.00, '#0a2133');
ocean.addColorStop(0.45, '#0e3350');
ocean.addColorStop(0.55, '#0e3350');
ocean.addColorStop(1.00, '#081a29');
ctx.fillStyle = ocean;
ctx.fillRect(0, 0, TEX_W, TEX_H);
/* 用三组正弦叠加出大陆轮廓,只求视觉上像陆地,不对应真实地理 */
function landValue(lonDeg, latDeg) {
const lon = lonDeg * Math.PI / 180;
const lat = latDeg * Math.PI / 180;
const a = Math.sin(lon * 2.2 + Math.cos(lat * 1.7) * 1.5);
const b = Math.sin(lon * 4.3 - lat * 2.9 + 0.7);
const c = Math.cos(lon * 1.2) * Math.cos(lat * 3.4);
return a * 0.5 + b * 0.28 + c * 0.22;
}
const img = ctx.getImageData(0, 0, TEX_W, TEX_H);
const data = img.data;
for (let y = 0; y < TEX_H; y++) {
const lat = 90 - (y + 0.5) / TEX_H * 180;
for (let x = 0; x < TEX_W; x++) {
const lon = (x + 0.5) / TEX_W * 360 - 180;
const v = landValue(lon, lat);
const idx = (y * TEX_W + x) * 4;
if (Math.abs(lat) > 71) {
/* 极区冰盖 */
data[idx] = 214;
data[idx + 1] = 228;
data[idx + 2] = 238;
data[idx + 3] = 255;
} else if (v > 0.13) {
/* 陆地:纬度越高越偏灰绿 */
const k = Math.min(1, Math.abs(lat) / 70);
data[idx] = Math.round(58 + 52 * k);
data[idx + 1] = Math.round(112 + 24 * k);
data[idx + 2] = Math.round(84 + 34 * k);
data[idx + 3] = 255;
}
}
}
ctx.putImageData(img, 0, 0);
/* 经纬网:每 30 度一条,赤道与本初子午线加亮 */
function gridLine(x1, y1, x2, y2, strong) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = strong ? 'rgba(150,205,245,0.55)' : 'rgba(130,180,220,0.22)';
ctx.lineWidth = strong ? 1.6 : 1;
ctx.stroke();
}
for (let lon = -180; lon <= 180; lon += 30) {
const x = (lon + 180) / 360 * TEX_W;
gridLine(x, 0, x, TEX_H, lon === 0);
}
for (let lat = -60; lat <= 60; lat += 30) {
const y = (90 - lat) / 180 * TEX_H;
gridLine(0, y, TEX_W, y, lat === 0);
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
/* ---------- 球体与标记 ---------- */
const GLOBE_R = 1.6;
const globe = new THREE.Mesh(
new THREE.SphereGeometry(GLOBE_R, 96, 64),
new THREE.MeshStandardMaterial({
map: texture,
roughness: 0.82,
metalness: 0.05
})
);
/* 让东经一百多度的中国区域朝向初始镜头 */
const earth = new THREE.Group();
earth.rotation.y = Math.PI;
earth.add(globe);
scene.add(earth);
/* 经纬度转球面直角坐标,lon 以本初子午线为 0,lat 北正南负 */
function lonLatToVec3(lon, lat, 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)
);
}
const cities = [
{ name: '北京', lon: 116.39, lat: 39.91, color: 0xffb454 },
{ name: '上海', lon: 121.47, lat: 31.23, color: 0x63e6be },
{ name: '广州', lon: 113.26, lat: 23.13, color: 0x8ab4ff }
];
const markerGroup = new THREE.Group();
const markerGeo = new THREE.SphereGeometry(0.022, 12, 12);
cities.forEach(function (city) {
const mat = new THREE.MeshBasicMaterial({ color: city.color });
const dot = new THREE.Mesh(markerGeo, mat);
dot.position.copy(lonLatToVec3(city.lon, city.lat, GLOBE_R * 1.005));
dot.userData.name = city.name;
/* 一根细杆把标记抬离球面,侧视时更醒目 */
const tip = lonLatToVec3(city.lon, city.lat, GLOBE_R * 1.08);
const stickGeo = new THREE.BufferGeometry().setFromPoints([dot.position, tip]);
const stick = new THREE.Line(
stickGeo,
new THREE.LineBasicMaterial({ color: city.color, transparent: true, opacity: 0.8 })
);
const cap = new THREE.Mesh(markerGeo, mat);
cap.position.copy(tip);
markerGroup.add(dot, stick, cap);
});
earth.add(markerGroup);
renderer.setAnimationLoop(function () {
controls.update();
renderer.render(scene, camera);
});
window.addEventListener('resize', function () {
const w = stage.clientWidth;
const h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
const spinBtn = document.getElementById('spin');
let spinning = true;
spinBtn.addEventListener('click', function () {
spinning = !spinning;
controls.autoRotate = spinning;
spinBtn.textContent = spinning ? '停止自转' : '开始自转';
});
const marksBtn = document.getElementById('marks');
let marksOn = true;
marksBtn.addEventListener('click', function () {
marksOn = !marksOn;
markerGroup.visible = marksOn;
marksBtn.textContent = marksOn ? '隐藏标记' : '显示标记';
});
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.