three.js实现3D地图的项目实践

 更新时间:2026年03月22日 11:44:30   作者:codingWhat  
three.js 是一个在浏览器里把 3D 画出来的图形库,本文就来详细的介绍一下three.js实现3D地图的项目实践,感兴趣的可以了解一下

Three.js

three.js 是一个在浏览器里把 3D 画出来的图形库。你可以把它想成搭舞台,那咋把舞台搭起来呢?

  1. Scene:搭舞台的“背景板”(灯光、模型、网格)。
  2. Camera:决定你从哪个方向看(透视相机更像“人眼”视角,正交相机更像“测绘”视角)。
  3. Renderer:负责“出画面”(每一帧把 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()

理解了这三件事,你再把“地区轮廓”转换成 3D 网格,就等于把数据变成了可以被光照和相机看到的模型。

实现流程

  1. 读取 GeoJSON:先把 Polygon 统一成和 MultiPolygon 一样的数据形态;
  2. 遍历每个 polygon ring:把点序列写进 THREE.Shape(moveTo/lineTo)
  3. 用 ExtrudeGeometry 把二维轮廓“拉起来”成三维实体(depth 决定高度,bevel 决定边缘质感)
  4. 给几何体分配材质组合:上表面更亮、侧边更有层次,立体感才会成立
  5. 用 Box3.expandByObject() 算中心与尺寸:一键对齐相机,让视角永远落在地图上
  6. 初始化 CSS2DRenderer:在动画循环里把 WebGL 和 2D 标签叠加起来,再驱动光柱/粒子等小动效

1) 统一 GeoJSON:让Polygon和MultiPolygon数据结构一致

很多 GeoJSON 在实际项目里会“有时是 Polygon,有时是 MultiPolygon”。如果你写死一套遍历逻辑,就会出现:某些地区根本没被绘制出来,或者逻辑分支越写越乱。

我是这样实现的:如果几何类型是 Polygon,就把 coordinates 包一层,让它变成 MultiPolygon 风格的二维数组。这样后续只写一套循环就够了。

// 统一 GeoJSON(关键点: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 }
}

数据与坐标:先想清楚你画的是“平面”还是“球面”

THREE.Shape 本质上只认“二维平面坐标”。所以第一件事不是写代码,而是先确认:你的 GeoJSON 点 (x, y) 在你的 Three.js 世界里,究竟应该落到哪里。

  • 如果你的数据已经是“平面化坐标”(例如直接用 GeoJSON 的 (x, y) 去描轮廓),那就可以直接 Shape -> ExtrudeGeometry,不用经纬度转换。
  • 如果你的数据是经纬度 (lon, lat),你就必须先做坐标转换。路线是:用球面映射把点投到球面上,再用四元数让面朝向球面法线。

2) 核心:从轮廓到 3D 面(Shape + ExtrudeGeometry)

这一段就是“把平面地图变成立体模型”的关键啦:只要你理解了它,后面的居中、标签、动效就都只是加配件。

在 Three.js 里:

  • THREE.Shape 负责把轮廓“描一遍”(moveTo/lineTo)
  • THREE.ExtrudeGeometry 负责把描好的轮廓“拉高变厚”(depth/bevel 等参数决定你要多立体)

我们可以把GeoJSON 的 ring 逐点喂给 Shape,再一口气拉伸成网格。注意:Mesh 的材质传数组是为了“上表面更亮、侧边更有阴影感”。

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 就手动调相机坐标,对吧?那就用包围盒做“自动聚焦”。

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)
}

效果会立刻变“稳定”:换地区数据也能落在视野正中。

4) 标签(CSS2D)与光柱

做到“有形”还不够,得让人看得懂。标签负责告诉你“这是什么”,光柱负责把注意力“引到那里”。

标签(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
}

渲染时只要每帧调用一次 css2dRender.render(scene, camera),标签就会“自动跟镜头走”。

光柱

光柱的漂亮之处在于“看起来更立体”,实现方案:两张切图交叉,再配合透明渲染参数避免穿帮。

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) 让动画活起来

你不需要把每一帧都烧到极致,但需要让“画面在动”,让它不显得生硬:

  • 背景光圈缓慢旋转
  • 粒子沿 z 轴上升再重置
  • 2D 标签每帧由 CSS2DRenderer 重新渲染

核心循环就四件小事:WebGL 渲染、2D 标签叠加、粒子/旋转等状态更新、以及 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()
}

踩过的坑分享给大家,少走些弯路

  1. 坐标系不匹配:THREE.Shape 只认平面坐标。你的 GeoJSON 如果和 Three.js 的绘制坐标不一致,就会出现“地图飞走了”的尴尬,需要先做投影/坐标转换。
  2. 空洞(holes)处理:把 ring 直接塞进 Shape,没有显式处理 shape.holes。一旦你的 GeoJSON 带内环(岛/湖泊/凹洞),不处理 holes 就会“该挖的地方没挖开”。
  3. bevel 参数太大:倒角太厚会让面数暴涨,性能变差。一般从小 bevel 开始试,满足质感再加料。
  4. 数据点顺序:点序自交或乱序时,Shape 可能生成失败,或者“看起来像被折弯”。这类问题通常要先检查几何数据本身。

到此这篇关于three.js实现3D地图的项目实践的文章就介绍到这了,更多相关three.js实现3D地图内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JS实现隐藏同级元素后只显示JS文件内容的方法

    JS实现隐藏同级元素后只显示JS文件内容的方法

    这篇文章主要介绍了JS实现隐藏同级元素后只显示JS文件内容的方法,可实现将与js文件的同级元素全部隐藏,只显示js文件内容的功能,涉及javascript针对页面元素的遍历与属性修改相关技巧,需要的朋友可以参考下
    2016-09-09
  • JS树形结构根据id获取父级节点元素的示例代码

    JS树形结构根据id获取父级节点元素的示例代码

    这篇文章主要介绍了JS树形结构根据id获取父级节点元素,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • Web打印解决方案之证件套打的实现思路

    Web打印解决方案之证件套打的实现思路

    这篇文章主要介绍了Web打印解决方案之证件套打的实现思路的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-08-08
  • php register_shutdown_function函数详解

    php register_shutdown_function函数详解

    register_shutdown_function() 函数可实现当程序执行完成后执行的函数,其功能为可实现程序执行完成的后续操作,需要的朋友可以参考下
    2017-07-07
  • JavaScript正则表达式和级联效果

    JavaScript正则表达式和级联效果

    正则表达式(regular expression)是一种字符串匹配的模式,用来检查一个字符串中是否包含指定模式的字符串。下面通过本文给大家分享JavaScript_正则表达式和级联效果,感兴趣的朋友一起看看吧
    2017-09-09
  • JavaScript数据结构Map的使用示例详解

    JavaScript数据结构Map的使用示例详解

    Map 是一种强大且灵活的数据结构,通过灵活使用 Map,可以更高效地处理复杂键类型和有序键值对的场景,提升代码可读性和性能,这篇文章主要介绍了JavaScript数据结构-Map的使用,需要的朋友可以参考下
    2025-05-05
  • 详解Bootstrap按钮

    详解Bootstrap按钮

    本文给大家介绍bootstrap按钮相关知识,包括按钮大小、按钮状态、按钮标签相关样式定义,对bootstrap按钮相关知识感兴趣的朋友一起学习吧
    2016-01-01
  • JavaScript之Object类型介绍

    JavaScript之Object类型介绍

    这篇文章主要介绍了JavaScript之Object类型介绍,本文讲解了创建Object类型的两种方法以及访问Object类型的方法,需要的朋友可以参考下
    2015-04-04
  • 如何在JavaScript中优雅的提取循环内数据详解

    如何在JavaScript中优雅的提取循环内数据详解

    这篇文章主要给大家介绍了关于如何在JavaScript中优雅的提取循环内数据的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-03-03
  • 浅谈es6中export和export default的作用及区别

    浅谈es6中export和export default的作用及区别

    下面小编就为大家分享一篇浅谈es6中export和export default的作用及区别,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-02-02

最新评论