使用Three.js创建交互式3D地球模型

使用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属性

在原实现中,我们使用lefttop属性来定位标签,这会导致频繁的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地球模型,并通过性能优化解决了标签显示不流畅的问题。通过这个项目,我们可以学到:

  1. Three.js的基本场景构建
  2. 3D几何体的创建和材质应用
  3. 地理坐标到3D坐标的转换
  4. DOM元素与3D场景的同步
  5. 性能优化技巧

这个3D地球模型不仅具有教育意义,还可以作为数据可视化、地理信息系统等项目的基础。通过进一步扩展,我们可以添加更多的地理信息、天气数据或实时信息,创建更加丰富的3D地球应用。

相关推荐
FL1717131414 小时前
excel转latex
人工智能
Aurora-Borealis.14 小时前
Day27 机器学习流水线
人工智能·机器学习
歌_顿14 小时前
知识蒸馏学习总结
人工智能·算法
老吴学AI15 小时前
系列报告九:(埃森哲)The New Rules of Platform Strategy in the Age of Agentic AI
人工智能
棒棒的皮皮15 小时前
【深度学习】YOLO模型速度优化Checklist
人工智能·深度学习·yolo·计算机视觉
意疏15 小时前
节点小宝4.0 正式发布:一键直达,重新定义远程控制!
人工智能
一个无名的炼丹师15 小时前
GraphRAG深度解析:从原理到实战,重塑RAG检索增强生成的未来
人工智能·python·rag
Yan-英杰16 小时前
BoostKit OmniAdaptor 源码深度解析
网络·人工智能·网络协议·tcp/ip·http
用泥种荷花16 小时前
【LangChain学习笔记】Message
人工智能