three.js實現(xiàn)3D地圖的項目實踐
Three.js
three.js 是一個在瀏覽器里把 3D 畫出來的圖形庫。你可以把它想成搭舞臺,那咋把舞臺搭起來呢?
Scene:搭舞臺的“背景板”(燈光、模型、網(wǎng)格)。Camera:決定你從哪個方向看(透視相機更像“人眼”視角,正交相機更像“測繪”視角)。Renderer:負責(zé)“出畫面”(每一幀把scene + camera渲染到畫布)。
看一眼最小骨架,后面不迷路:
import * as THREE from "three"
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
camera.position.set(0, 0, 10)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
container.appendChild(renderer.domElement)
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
理解了這三件事,你再把“地區(qū)輪廓”轉(zhuǎn)換成 3D 網(wǎng)格,就等于把數(shù)據(jù)變成了可以被光照和相機看到的模型。
實現(xiàn)流程
- 讀取 GeoJSON:先把 Polygon 統(tǒng)一成和 MultiPolygon 一樣的數(shù)據(jù)形態(tài);
- 遍歷每個 polygon ring:把點序列寫進 THREE.Shape(moveTo/lineTo)
- 用 ExtrudeGeometry 把二維輪廓“拉起來”成三維實體(depth 決定高度,bevel 決定邊緣質(zhì)感)
- 給幾何體分配材質(zhì)組合:上表面更亮、側(cè)邊更有層次,立體感才會成立
- 用 Box3.expandByObject() 算中心與尺寸:一鍵對齊相機,讓視角永遠落在地圖上
- 初始化 CSS2DRenderer:在動畫循環(huán)里把 WebGL 和 2D 標簽疊加起來,再驅(qū)動光柱/粒子等小動效
1) 統(tǒng)一 GeoJSON:讓Polygon和MultiPolygon數(shù)據(jù)結(jié)構(gòu)一致
很多 GeoJSON 在實際項目里會“有時是 Polygon,有時是 MultiPolygon”。如果你寫死一套遍歷邏輯,就會出現(xiàn):某些地區(qū)根本沒被繪制出來,或者邏輯分支越寫越亂。
我是這樣實現(xiàn)的:如果幾何類型是 Polygon,就把 coordinates 包一層,讓它變成 MultiPolygon 風(fēng)格的二維數(shù)組。這樣后續(xù)只寫一套循環(huán)就夠了。
// 統(tǒng)一 GeoJSON(關(guān)鍵點:Polygon -> MultiPolygon-like)
export default function useConversionStandardData() {
const transfromGeoJSON = (worldData) => {
const features = worldData.features
for (let i = 0; i < features.length; i++) {
const element = features[i]
if (element.geometry.type === "Polygon") {
// Polygon: [ [ [x,y], ... ] ]
// 包一層 -> MultiPolygon-like: [ [ [x,y], ... ] ]
element.geometry.coordinates = [element.geometry.coordinates]
}
}
return worldData
}
return { transfromGeoJSON }
}數(shù)據(jù)與坐標:先想清楚你畫的是“平面”還是“球面”
THREE.Shape 本質(zhì)上只認“二維平面坐標”。所以第一件事不是寫代碼,而是先確認:你的 GeoJSON 點 (x, y) 在你的 Three.js 世界里,究竟應(yīng)該落到哪里。
- 如果你的數(shù)據(jù)已經(jīng)是“平面化坐標”(例如直接用 GeoJSON 的 (x, y) 去描輪廓),那就可以直接 Shape -> ExtrudeGeometry,不用經(jīng)緯度轉(zhuǎn)換。
- 如果你的數(shù)據(jù)是經(jīng)緯度 (lon, lat),你就必須先做坐標轉(zhuǎn)換。路線是:用球面映射把點投到球面上,再用四元數(shù)讓面朝向球面法線。
2) 核心:從輪廓到 3D 面(Shape + ExtrudeGeometry)
這一段就是“把平面地圖變成立體模型”的關(guān)鍵啦:只要你理解了它,后面的居中、標簽、動效就都只是加配件。
在 Three.js 里:
- THREE.Shape 負責(zé)把輪廓“描一遍”(moveTo/lineTo)
- THREE.ExtrudeGeometry 負責(zé)把描好的輪廓“拉高變厚”(depth/bevel 等參數(shù)決定你要多立體)
我們可以把GeoJSON 的 ring 逐點喂給 Shape,再一口氣拉伸成網(wǎng)格。注意:Mesh 的材質(zhì)傳數(shù)組是為了“上表面更亮、側(cè)邊更有陰影感”。
import * as THREE from "three"
const extrudeSettings = { depth: 0.2, bevelEnabled: true, bevelSegments: 1, bevelThickness: 0.1 }
function buildRegion3D({ geoJson, topFaceMaterial, sideMaterial }) {
const mapGroup = new THREE.Group()
geoJson.features.forEach((feature) => {
const province = new THREE.Object3D()
const coordinates = feature.geometry.coordinates
coordinates.forEach((multiPolygon) => {
multiPolygon.forEach((polygon) => {
const shape = new THREE.Shape()
polygon.forEach(([x, y], i) => (i === 0 ? shape.moveTo(x, y) : shape.lineTo(x, y)))
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
province.add(new THREE.Mesh(geometry, [topFaceMaterial, sideMaterial]))
})
})
mapGroup.add(province)
})
return mapGroup
}3) 自動居中
你也不想每換一份 GeoJSON 就手動調(diào)相機坐標,對吧?那就用包圍盒做“自動聚焦”。
Box3().expandByObject() 會把整個地圖包起來,你拿到中心點以后就可以:讓相機 lookAt 它,控制器 target 也跟著指向它。
import * as THREE from "three"
function initCameraTargetByBox({ group, camera, controls }) {
const box3 = new THREE.Box3().expandByObject(group)
const center = new THREE.Vector3()
box3.getCenter(center)
camera.lookAt(center.x, center.y, 0)
if (controls?.target) controls.target = new THREE.Vector3(center.x, center.y, 0)
}
效果會立刻變“穩(wěn)定”:換地區(qū)數(shù)據(jù)也能落在視野正中。
4) 標簽(CSS2D)與光柱
做到“有形”還不夠,得讓人看得懂。標簽負責(zé)告訴你“這是什么”,光柱負責(zé)把注意力“引到那里”。
標簽(CSS2DRenderer)
這里我沒有做3D字體幾何體(太重也太麻煩),而是用 HTML div 作為“貼紙”。CSS2DObject 讓它能跟隨相機正確投影。
import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer"
const create2DTag = (html, className = "") => {
const div = document.createElement("div")
div.innerHTML = html
div.className = className
div.style.pointerEvents = "none"
div.style.visibility = "hidden"
const label = new CSS2DObject(div)
label.show = (text, point) => {
label.element.innerHTML = text
label.element.style.visibility = "visible"
label.position.copy(point)
}
label.hide = () => { label.element.style.visibility = "hidden" }
return label
}
渲染時只要每幀調(diào)用一次 css2dRender.render(scene, camera),標簽就會“自動跟鏡頭走”。
光柱
光柱的漂亮之處在于“看起來更立體”,實現(xiàn)方案:兩張切圖交叉,再配合透明渲染參數(shù)避免穿幫。
import * as THREE from "three"
// textureLoader / 紋理 url 在外層準備
const createLightPillar = (lon, lat, height, textureUrl, color = 0x00ffff) => {
const group = new THREE.Group()
const geometry = new THREE.PlaneBufferGeometry(height / 6.219, height)
geometry.rotateX(Math.PI / 2)
geometry.translate(0, 0, height / 2)
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(textureUrl),
color,
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
})
const a = new THREE.Mesh(geometry, material)
const b = a.clone()
b.rotateZ(Math.PI / 2)
group.add(a, b)
group.position.set(lon, lat, 0)
return group
}
地圖面生成后把光柱加進 mapGroup 就行
const light = createLightPillar(...lightCenter, heightScaleFactor, lightPillarTextureUrl) light.position.z = 0.31 mapGroup.add(light)
5) 讓動畫活起來
你不需要把每一幀都燒到極致,但需要讓“畫面在動”,讓它不顯得生硬:
- 背景光圈緩慢旋轉(zhuǎn)
- 粒子沿 z 軸上升再重置
- 2D 標簽每幀由 CSS2DRenderer 重新渲染
核心循環(huán)就四件小事:WebGL 渲染、2D 標簽疊加、粒子/旋轉(zhuǎn)等狀態(tài)更新、以及 TWEEN.update() 推進動畫:
loop() {
requestAnimationFrame(() => this.loop())
this.renderer.render(this.scene, this.camera)
if (this.rotatingApertureMesh) this.rotatingApertureMesh.rotation.z += 0.0005
if (this.css2dRender) this.css2dRender.render(this.scene, this.camera)
for (const p of this.particleArr || []) {
p.updateSequenceFrame()
p.position.z += 0.01
if (p.position.z >= 6) p.position.z = -6
}
TWEEN.update()
}
踩過的坑分享給大家,少走些彎路
- 坐標系不匹配:THREE.Shape 只認平面坐標。你的 GeoJSON 如果和 Three.js 的繪制坐標不一致,就會出現(xiàn)“地圖飛走了”的尷尬,需要先做投影/坐標轉(zhuǎn)換。
- 空洞(holes)處理:把 ring 直接塞進 Shape,沒有顯式處理 shape.holes。一旦你的 GeoJSON 帶內(nèi)環(huán)(島/湖泊/凹洞),不處理 holes 就會“該挖的地方?jīng)]挖開”。
- bevel 參數(shù)太大:倒角太厚會讓面數(shù)暴漲,性能變差。一般從小 bevel 開始試,滿足質(zhì)感再加料。
- 數(shù)據(jù)點順序:點序自交或亂序時,Shape 可能生成失敗,或者“看起來像被折彎”。這類問題通常要先檢查幾何數(shù)據(jù)本身。
到此這篇關(guān)于three.js實現(xiàn)3D地圖的項目實踐的文章就介紹到這了,更多相關(guān)three.js實現(xiàn)3D地圖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
理解?JavaScript?對象屬性訪問的復(fù)雜性(示例代碼)
在?JavaScript?編程中,開發(fā)者經(jīng)常需要對對象的屬性進行訪問,然而,訪問方式的不同可能導(dǎo)致代碼行為的差異,尤其在動態(tài)屬性的處理中,本文介紹JavaScript對象屬性訪問的復(fù)雜性,感興趣的朋友跟隨小編一起看看吧2024-12-12
JS/HTML5游戲常用算法之碰撞檢測 包圍盒檢測算法詳解【圓形情況】
這篇文章主要介紹了JS/HTML5游戲常用算法之碰撞檢測 包圍盒檢測算法,結(jié)合實例形式詳細分析了圓形包盒情況下的碰撞檢測算法相關(guān)原理與實現(xiàn)技巧,需要的朋友可以參考下2018-12-12
js 將json字符串轉(zhuǎn)換為json對象的方法解析
將json字符串轉(zhuǎn)換為json對象的方法。在數(shù)據(jù)傳輸過程中,json是以文本,即字符串的形式傳遞的,而JS操作的是JSON對象,所以,JSON對象和JSON字符串之間的相互轉(zhuǎn)換是關(guān)鍵2013-11-11
JavaScript實現(xiàn)仿Clock ISO時鐘
這篇文章給大家分享了JavaScript實現(xiàn)仿Clock ISO時鐘的方法以及實例代碼,有興趣的朋友參考學(xué)習(xí)下下。2018-06-06

