詳細(xì)介紹如何使用Three.js創(chuàng)建交互式3D地球模型
前言
在現(xiàn)代Web開(kāi)發(fā)中,3D圖形可視化已經(jīng)成為一個(gè)熱門(mén)話(huà)題。Three.js作為最流行的3D庫(kù)之一,為我們提供了強(qiáng)大的工具來(lái)創(chuàng)建引人入勝的3D場(chǎng)景。本文將詳細(xì)介紹如何使用Three.js創(chuàng)建一個(gè)交互式的3D地球模型,并逐步優(yōu)化其性能,最終實(shí)現(xiàn)一個(gè)帶有國(guó)家名稱(chēng)標(biāo)簽的流暢3D地球。
一、Three.js簡(jiǎn)介
Three.js是一個(gè)基于WebGL的JavaScript 3D庫(kù),它封裝了復(fù)雜的WebGL API,讓開(kāi)發(fā)者能夠更輕松地創(chuàng)建和展示3D場(chǎng)景。Three.js提供了豐富的幾何體、材質(zhì)、燈光和相機(jī)等組件,使得3D開(kāi)發(fā)變得更加簡(jiǎn)單。
二、基礎(chǔ)3D地球模型構(gòu)建
2.1 初始化場(chǎng)景
首先,我們需要?jiǎng)?chuàng)建一個(gè)基本的Three.js場(chǎng)景:
// 創(chuàng)建場(chǎng)景
scene = new THREE.Scene();
// 創(chuàng)建相機(jī)
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 2.5;
// 創(chuàng)建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);2.2 創(chuàng)建地球幾何體
接下來(lái),我們創(chuàng)建一個(gè)球體作為地球的基礎(chǔ)幾何體:
// 創(chuàng)建地球幾何體
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');
// 創(chuàng)建材質(zhì)
const material = new THREE.MeshPhongMaterial({
map: map,
bumpMap: bumpMap,
bumpScale: 0.05,
specularMap: specularMap,
specular: new THREE.Color(0x333333),
shininess: 5
});
// 創(chuàng)建地球網(wǎng)格
earth = new THREE.Mesh(geometry, material);
earth.position.set(0, 0, 0);
scene.add(earth);三、添加交互功能
為了讓地球可以旋轉(zhuǎn),我們需要實(shí)現(xiàn)鼠標(biāo)拖拽功能:
// 鼠標(biāo)按下處理
function onMouseDown(event) {
isDragging = true;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
// 鼠標(biāo)移動(dòng)處理
function onMouseMove(event) {
if (isDragging) {
const deltaX = event.clientX - previousMousePosition.x;
const deltaY = event.clientY - previousMousePosition.y;
// 更新地球旋轉(zhuǎn)
earth.rotation.y += deltaX * 0.01;
earth.rotation.x += deltaY * 0.01;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
}四、添加國(guó)家名稱(chēng)標(biāo)簽
4.1 坐標(biāo)轉(zhuǎn)換
要將地理坐標(biāo)轉(zhuǎn)換為3D坐標(biāo),我們需要實(shí)現(xiàn)經(jīng)緯度到向量的轉(zhuǎn)換:
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 標(biāo)簽可見(jiàn)性判斷
為了確保標(biāo)簽只在地球正面顯示,我們需要計(jì)算標(biāo)簽與相機(jī)的相對(duì)位置:
// 只有當(dāng)前面可見(jiàn)時(shí)才顯示標(biāo)簽
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) { // 標(biāo)簽在地球正面
// 顯示標(biāo)簽
} else {
// 隱藏標(biāo)簽
}五、性能優(yōu)化
5.1 使用CSS Transform替代DOM屬性
在原實(shí)現(xiàn)中,我們使用left和top屬性來(lái)定位標(biāo)簽,這會(huì)導(dǎo)致頻繁的DOM重排。優(yōu)化后使用transform屬性:
// 使用transform替代left/top,提高性能
labelObj.element.style.transform = `translate(${x - labelObj.element.offsetWidth / 2}px, ${y - labelObj.element.offsetHeight}px)`;5.2 減少不必要的DOM操作
通過(guò)跟蹤標(biāo)簽的可見(jiàn)狀態(tài),避免重復(fù)設(shè)置相同的顯示狀態(tài):
if (!labelObj.visible) {
labelObj.element.style.display = 'block';
labelObj.visible = true;
}六、完整代碼實(shí)現(xiàn)
以下是完整的交互式3D地球模型代碼:
<!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">拖動(dòng)地球旋轉(zhuǎn) | 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>
// 基本設(shè)置
let scene, camera, renderer;
let earth;
let isDragging = false;
let previousMousePosition = {
x: 0,
y: 0
};
let labelElements = [];
// 國(guó)家數(shù)據(jù) - 經(jīng)緯度坐標(biāo)
const countries = [
{ name: "中國(guó)", lat: 35.8617, lng: 104.1954 },
{ name: "美國(guó)", 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: "英國(guó)", lat: 55.3781, lng: -3.4360 },
{ name: "法國(guó)", lat: 46.2276, lng: 2.2137 },
{ name: "德國(guó)", 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 }
];
// 將經(jīng)緯度轉(zhuǎn)換為3D坐標(biāo)
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)
);
}
// 初始化場(chǎng)景
function init() {
// 創(chuàng)建場(chǎng)景
scene = new THREE.Scene();
// 創(chuàng)建相機(jī)
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 2.5;
// 創(chuàng)建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// 創(chuàng)建地球幾何體
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');
// 創(chuàng)建材質(zhì)
const material = new THREE.MeshPhongMaterial({
map: map,
bumpMap: bumpMap,
bumpScale: 0.05,
specularMap: specularMap,
specular: new THREE.Color(0x333333),
shininess: 5
});
// 創(chuàng)建地球網(wǎng)格
earth = new THREE.Mesh(geometry, material);
earth.position.set(0, 0, 0);
scene.add(earth);
// 添加國(guó)家標(biāo)簽
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);
// 綁定事件監(jiān)聽(tīng)器
bindEventListeners();
// 開(kāi)始動(dòng)畫(huà)循環(huán)
animate();
}
// 創(chuàng)建國(guó)家標(biāo)簽
function createCountryLabels() {
// 為每個(gè)國(guó)家創(chuàng)建一個(gè)標(biāo)簽元素
countries.forEach(country => {
const label = document.createElement('div');
label.className = 'country-label';
label.textContent = country.name;
label.style.display = 'none';
document.body.appendChild(label);
// 計(jì)算3D位置
const position = latLongToVector3(country.lat, country.lng, 1.02);
labelElements.push({
element: label,
position: position,
country: country,
visible: false
});
});
}
// 更新標(biāo)簽位置
function updateLabelPositions() {
const vector = new THREE.Vector3();
const canvas = renderer.domElement;
labelElements.forEach(labelObj => {
// 將3D位置轉(zhuǎn)換為屏幕坐標(biāo)
vector.copy(labelObj.position);
vector.applyEuler(earth.rotation);
vector.project(camera);
// 計(jì)算屏幕坐標(biāo)
const x = Math.round((vector.x * 0.5 + 0.5) * canvas.clientWidth);
const y = Math.round(( -vector.y * 0.5 + 0.5) * canvas.clientHeight);
// 只有當(dāng)前面可見(jiàn)時(shí)才顯示標(biāo)簽
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;
}
}
});
}
// 綁定事件監(jiān)聽(tīng)器
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);
}
// 鼠標(biāo)按下處理
function onMouseDown(event) {
isDragging = true;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
// 鼠標(biāo)移動(dòng)處理
function onMouseMove(event) {
if (isDragging) {
const deltaX = event.clientX - previousMousePosition.x;
const deltaY = event.clientY - previousMousePosition.y;
// 更新地球旋轉(zhuǎn)
earth.rotation.y += deltaX * 0.01;
earth.rotation.x += deltaY * 0.01;
previousMousePosition = {
x: event.clientX,
y: event.clientY
};
}
}
// 鼠標(biāo)抬起或離開(kāi)處理
function onMouseUp() {
isDragging = false;
}
// 窗口大小調(diào)整處理
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
// 重新計(jì)算標(biāo)簽位置
setTimeout(updateLabelPositions, 100);
}
// 動(dòng)畫(huà)循環(huán)
function animate() {
requestAnimationFrame(animate);
// 非拖動(dòng)狀態(tài)下自動(dòng)旋轉(zhuǎn)
if (!isDragging) {
earth.rotation.y += 0.001;
}
// 更新標(biāo)簽位置
updateLabelPositions();
renderer.render(scene, camera);
}
// 啟動(dòng)應(yīng)用
init();
</script>
</body>
</html>七、總結(jié)
本文詳細(xì)介紹了如何使用Three.js創(chuàng)建一個(gè)交互式的3D地球模型,并通過(guò)性能優(yōu)化解決了標(biāo)簽顯示不流暢的問(wèn)題。通過(guò)這個(gè)項(xiàng)目,我們可以學(xué)到:
- Three.js的基本場(chǎng)景構(gòu)建
- 3D幾何體的創(chuàng)建和材質(zhì)應(yīng)用
- 地理坐標(biāo)到3D坐標(biāo)的轉(zhuǎn)換
- DOM元素與3D場(chǎng)景的同步
- 性能優(yōu)化技巧
這個(gè)3D地球模型不僅具有教育意義,還可以作為數(shù)據(jù)可視化、地理信息系統(tǒng)等項(xiàng)目的基礎(chǔ)。通過(guò)進(jìn)一步擴(kuò)展,我們可以添加更多的地理信息、天氣數(shù)據(jù)或?qū)崟r(shí)信息,創(chuàng)建更加豐富的3D地球應(yīng)用。
到此這篇關(guān)于如何使用Three.js創(chuàng)建交互式3D地球模型的文章就介紹到這了,更多相關(guān)Three.js交互式3D地球模型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript 另一種圖片滾動(dòng)切換效果思路
把圖片們用ul之類(lèi)的包起來(lái),并設(shè)置float。然后設(shè)置這個(gè)ul本身為absolute定位,其父標(biāo)簽用relative定位。通過(guò)設(shè)置ul的left或top值,實(shí)現(xiàn)圖片隊(duì)列的滾動(dòng)效果2012-04-04
JavaScript實(shí)現(xiàn)京東快遞單號(hào)查詢(xún)
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)京東快遞單號(hào)查詢(xún),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
下雪了 javascript實(shí)現(xiàn)雪花飛舞
下雪了,這篇文章主要介紹了javascript實(shí)現(xiàn)雪花飛舞,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-04-04
ES6新特性三: Generator(生成器)函數(shù)詳解
這篇文章主要介紹了ES6新特性之Generator(生成器)函數(shù),簡(jiǎn)單分析了Generator(生成器)函數(shù)的功能、定義、調(diào)用方法并結(jié)合實(shí)例形式給出了相關(guān)使用技巧,需要的朋友可以參考下2017-04-04
如何HttpServletRequest文件對(duì)象并儲(chǔ)存
這篇文章主要介紹了如何HttpServletRequest文件對(duì)象并儲(chǔ)存,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
uni-app頁(yè)面?zhèn)鲄⒖倎G值的3種解決方法
文章介紹了三種在前端頁(yè)面間傳遞參數(shù)的方法:URL傳參、全局變量傳參和本地存儲(chǔ)傳參,每種方法都有其適用場(chǎng)景和注意事項(xiàng),需要的朋友可以參考下2025-12-12
ie支持function.bind()方法實(shí)現(xiàn)代碼
在 google 一番技術(shù)資料后,發(fā)現(xiàn) firefox 原生支持一個(gè) bind 方法,該方法很好的滿(mǎn)足了我們的初衷,調(diào)用方法與 call 和 apply 一樣,只是定義完成后,在后期調(diào)用時(shí)該方法才會(huì)執(zhí)行,需要的朋友可以了解下2012-12-12
“不能執(zhí)行已釋放的Script代碼”錯(cuò)誤的原因及解決辦法
“不能執(zhí)行已釋放的Script代碼”錯(cuò)誤的原因及解決辦法...2007-09-09
JS簡(jiǎn)單實(shí)現(xiàn)城市二級(jí)聯(lián)動(dòng)選擇插件的方法
這篇文章主要介紹了JS簡(jiǎn)單實(shí)現(xiàn)城市二級(jí)聯(lián)動(dòng)選擇插件的方法,涉及javascript實(shí)現(xiàn)select遍歷與設(shè)置技巧,非常簡(jiǎn)單實(shí)用,需要的朋友可以參考下2015-08-08

