使用Three.js创建交互式3D地球模型
前言
在现代Web开发中,3D图形可视化已经成为一个热门话题。Three.js作为最流行的3D库之一,为我们提供了强大的工具来创建引人入胜的3D场景。本文将详细介绍如何使用Three.js创建一个交互式的3D地球模型,并逐步优化其性能,最终实现一个带有国家名称标签的流畅3D地球。
一、Three.js简介
Three.js是一个基于WebGL的JavaScript 3D库,它封装了复杂的WebGL API,让开发者能够更轻松地创建和展示3D场景。Three.js提供了丰富的几何体、材质、灯光和相机等组件,使得3D开发变得更加简单。
二、基础3D地球模型构建
2.1 初始化场景
首先,我们需要创建一个基本的Three.js场景:
javascript
// 创建场景
scene = new THREE.Scene();
// 创建相机
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 2.5;
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
2.2 创建地球几何体
接下来,我们创建一个球体作为地球的基础几何体:
javascript
// 创建地球几何体
const geometry = new THREE.SphereGeometry(1, 64, 64);
// 加载贴图
const textureLoader = new THREE.TextureLoader();
const map = textureLoader.load('https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg');
const bumpMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_normal_2048.jpg');
const specularMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_specular_2048.jpg');
// 创建材质
const material = new THREE.MeshPhongMaterial({
map: map,
bumpMap: bumpMap,
bumpScale: 0.05,
specularMap: specularMap,
specular: new THREE.Color(0x333333),
shininess: 5
});
// 创建地球网格
earth = new THREE.Mesh(geometry, material);
earth.position.set(0, 0, 0);
scene.add(earth);
三、添加交互功能
为了让地球可以旋转,我们需要实现鼠标拖拽功能:
javascript
// 鼠标按下处理
function onMouseDown(event) {
isDragging = true;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
// 鼠标移动处理
function onMouseMove(event) {
if (isDragging) {
const deltaX = event.clientX - previousMousePosition.x;
const deltaY = event.clientY - previousMousePosition.y;
// 更新地球旋转
earth.rotation.y += deltaX * 0.01;
earth.rotation.x += deltaY * 0.01;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
}
四、添加国家名称标签
4.1 坐标转换
要将地理坐标转换为3D坐标,我们需要实现经纬度到向量的转换:
javascript
function latLongToVector3(lat, lng, radius) {
const phi = (90 - lat) * Math.PI / 180;
const theta = (lng + 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)
);
}
4.2 标签可见性判断
为了确保标签只在地球正面显示,我们需要计算标签与相机的相对位置:
javascript
// 只有当前面可见时才显示标签
const cameraVector = new THREE.Vector3().subVectors(earth.position, camera.position).normalize();
const labelVector = new THREE.Vector3().copy(labelObj.position).applyEuler(earth.rotation);
const dotProduct = cameraVector.dot(labelVector);
if (dotProduct < 0) { // 标签在地球正面
// 显示标签
} else {
// 隐藏标签
}
五、性能优化
5.1 使用CSS Transform替代DOM属性
在原实现中,我们使用left和top属性来定位标签,这会导致频繁的DOM重排。优化后使用transform属性:
javascript
// 使用transform替代left/top,提高性能
labelObj.element.style.transform = `translate(${x - labelObj.element.offsetWidth / 2}px, ${y - labelObj.element.offsetHeight}px)`;
5.2 减少不必要的DOM操作
通过跟踪标签的可见状态,避免重复设置相同的显示状态:
javascript
if (!labelObj.visible) {
labelObj.element.style.display = 'block';
labelObj.visible = true;
}
六、完整代码实现
以下是完整的交互式3D地球模型代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D地球模型</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000000;
font-family: Arial, sans-serif;
}
#container {
position: absolute;
width: 100%;
height: 100%;
}
#instructions {
position: absolute;
top: 20px;
width: 100%;
text-align: center;
color: white;
font-size: 14px;
font-family: Arial, sans-serif;
text-shadow: 2px 2px 4px #000000;
pointer-events: none;
z-index: 10;
}
.country-label {
position: absolute;
color: white;
font-size: 12px;
font-family: Arial, sans-serif;
text-shadow: 1px 1px 2px black;
pointer-events: none;
white-space: nowrap;
z-index: 100;
transition: transform 0.1s ease;
}
</style>
</head>
<body>
<div id="instructions">拖动地球旋转 | Drag to rotate the Earth</div>
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// 基本设置
let scene, camera, renderer;
let earth;
let isDragging = false;
let previousMousePosition = {
x: 0,
y: 0
};
let labelElements = [];
// 国家数据 - 经纬度坐标
const countries = [
{ name: "中国", lat: 35.8617, lng: 104.1954 },
{ name: "美国", lat: 37.0902, lng: -95.7129 },
{ name: "俄罗斯", lat: 61.5240, lng: 105.3188 },
{ name: "加拿大", lat: 56.1304, lng: -106.3468 },
{ name: "巴西", lat: -14.2350, lng: -51.9253 },
{ name: "澳大利亚", lat: -25.2744, lng: 133.7751 },
{ name: "印度", lat: 20.5937, lng: 78.9629 },
{ name: "英国", lat: 55.3781, lng: -3.4360 },
{ name: "法国", lat: 46.2276, lng: 2.2137 },
{ name: "德国", lat: 51.1657, lng: 10.4515 },
{ name: "日本", lat: 36.2048, lng: 138.2529 },
{ name: "阿根廷", lat: -38.4161, lng: -63.6167 },
{ name: "埃及", lat: 26.0975, lng: 30.0444 },
{ name: "南非", lat: -30.5595, lng: 22.9375 },
{ name: "沙特", lat: 23.8859, lng: 45.0792 }
];
// 将经纬度转换为3D坐标
function latLongToVector3(lat, lng, radius) {
const phi = (90 - lat) * Math.PI / 180;
const theta = (lng + 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 init() {
// 创建场景
scene = new THREE.Scene();
// 创建相机
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 2.5;
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// 创建地球几何体
const geometry = new THREE.SphereGeometry(1, 64, 64);
// 加载贴图
const textureLoader = new THREE.TextureLoader();
const map = textureLoader.load('https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg');
const bumpMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_normal_2048.jpg');
const specularMap = textureLoader.load('https://threejs.org/examples/textures/planets/earth_specular_2048.jpg');
// 创建材质
const material = new THREE.MeshPhongMaterial({
map: map,
bumpMap: bumpMap,
bumpScale: 0.05,
specularMap: specularMap,
specular: new THREE.Color(0x333333),
shininess: 5
});
// 创建地球网格
earth = new THREE.Mesh(geometry, material);
earth.position.set(0, 0, 0);
scene.add(earth);
// 添加国家标签
createCountryLabels();
// 添加灯光
const ambientLight = new THREE.AmbientLight(0x404040, 1);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(2, 1, 3).normalize();
scene.add(directionalLight);
// 绑定事件监听器
bindEventListeners();
// 开始动画循环
animate();
}
// 创建国家标签
function createCountryLabels() {
// 为每个国家创建一个标签元素
countries.forEach(country => {
const label = document.createElement('div');
label.className = 'country-label';
label.textContent = country.name;
label.style.display = 'none';
document.body.appendChild(label);
// 计算3D位置
const position = latLongToVector3(country.lat, country.lng, 1.02);
labelElements.push({
element: label,
position: position,
country: country,
visible: false
});
});
}
// 更新标签位置
function updateLabelPositions() {
const vector = new THREE.Vector3();
const canvas = renderer.domElement;
labelElements.forEach(labelObj => {
// 将3D位置转换为屏幕坐标
vector.copy(labelObj.position);
vector.applyEuler(earth.rotation);
vector.project(camera);
// 计算屏幕坐标
const x = Math.round((vector.x * 0.5 + 0.5) * canvas.clientWidth);
const y = Math.round(( -vector.y * 0.5 + 0.5) * canvas.clientHeight);
// 只有当前面可见时才显示标签
const cameraVector = new THREE.Vector3().subVectors(earth.position, camera.position).normalize();
const labelVector = new THREE.Vector3().copy(labelObj.position).applyEuler(earth.rotation);
const dotProduct = cameraVector.dot(labelVector);
if (dotProduct < 0) {
if (!labelObj.visible) {
labelObj.element.style.display = 'block';
labelObj.visible = true;
}
labelObj.element.style.transform = `translate(${x - labelObj.element.offsetWidth / 2}px, ${y - labelObj.element.offsetHeight}px)`;
} else {
if (labelObj.visible) {
labelObj.element.style.display = 'none';
labelObj.visible = false;
}
}
});
}
// 绑定事件监听器
function bindEventListeners() {
renderer.domElement.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mousemove', onMouseMove, false);
window.addEventListener('mouseup', onMouseUp, false);
renderer.domElement.addEventListener('mouseleave', onMouseUp, false);
window.addEventListener('resize', onWindowResize, false);
renderer.domElement.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
}
// 鼠标按下处理
function onMouseDown(event) {
isDragging = true;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
// 鼠标移动处理
function onMouseMove(event) {
if (isDragging) {
const deltaX = event.clientX - previousMousePosition.x;
const deltaY = event.clientY - previousMousePosition.y;
// 更新地球旋转
earth.rotation.y += deltaX * 0.01;
earth.rotation.x += deltaY * 0.01;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
}
// 鼠标抬起或离开处理
function onMouseUp() {
isDragging = false;
}
// 窗口大小调整处理
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
// 重新计算标签位置
setTimeout(updateLabelPositions, 100);
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 非拖动状态下自动旋转
if (!isDragging) {
earth.rotation.y += 0.001;
}
// 更新标签位置
updateLabelPositions();
renderer.render(scene, camera);
}
// 启动应用
init();
</script>
</body>
</html>
七、总结
本文详细介绍了如何使用Three.js创建一个交互式的3D地球模型,并通过性能优化解决了标签显示不流畅的问题。通过这个项目,我们可以学到:
- Three.js的基本场景构建
- 3D几何体的创建和材质应用
- 地理坐标到3D坐标的转换
- DOM元素与3D场景的同步
- 性能优化技巧
这个3D地球模型不仅具有教育意义,还可以作为数据可视化、地理信息系统等项目的基础。通过进一步扩展,我们可以添加更多的地理信息、天气数据或实时信息,创建更加丰富的3D地球应用。