
项目概述
这是一个基于Three.js的3D交互式地图可视化系统,以广东省地图为展示对象,实现了丰富的3D视觉效果和交互功能。本文将对项目中的核心函数进行逐步骤、逐函数的详细分析,帮助读者深入理解系统的实现原理。
技术栈
- 前端框架:Vue 3
- 3D渲染引擎:Three.js
- 构建工具:Vite
- 动画库:Tween.js
- 辅助库:Delaunator、geo-point-in-polygon等地理计算库
项目初始化流程
1. App.vue - 主组件入口
onMounted - 组件挂载函数
javascript
onMounted(async () => {
// 1. 加载地图数据
let provinceData = await requestData("./data/map/广东省.json")
provinceData = transfromGeoJSON(provinceData)
// 2. 继承Map3d类创建当前地图实例
class CurrentMap3d extends Map3d {
// ... 自定义地图方法
}
// 3. 初始化地图实例
baseMap = new CurrentMap3d({
container: "#app-32-map",
axesVisibel: true,
controls: {
enableDamping: true,
maxPolarAngle: (Math.PI / 2) * 0.98,
},
})
// 4. 运行地图
baseMap.run()
// 5. 添加窗口大小变化监听
window.addEventListener("resize", resize)
})
作用:组件挂载时执行,完成地图的初始化、数据加载、渲染和事件监听设置。
执行步骤:
- 加载并转换广东省地图数据
- 定义自定义地图类继承Map3d基类
- 创建地图实例并配置参数
- 运行地图渲染循环
- 添加窗口大小变化监听
数据处理模块
1. useFileLoader.js - 文件加载钩子
requestData - 异步数据请求函数
javascript
const requestData = async (url) => {
try {
const response = await fetch(url)
const data = await response.json()
return data
} catch (error) {
console.error('数据加载失败:', error)
return null
}
}
作用:异步加载GeoJSON地图数据。
参数:
url:地图数据文件路径
返回值:解析后的JSON数据对象
2. useConversionStandardData.js - 数据格式转换钩子
transfromGeoJSON - GeoJSON数据转换函数
javascript
const transfromGeoJSON = (worldData) => {
let features = worldData.features
for (let i = 0; i < features.length; i++) {
const element = features[i]
// 将Polygon处理跟MultiPolygon一样的数据结构
if (element.geometry.type === 'Polygon') {
element.geometry.coordinates = [element.geometry.coordinates]
}
}
return worldData
}
作用:统一GeoJSON数据格式,将Polygon类型数据转换为与MultiPolygon相同的二维数组结构。
参数:
worldData:原始GeoJSON数据
返回值:标准化后的GeoJSON数据
实现原理:遍历features数组,检测geometry.type,如果是Polygon类型,则将coordinates转换为二维数组格式,确保后续处理的一致性。
3. useCoord.js - 坐标处理钩子
geoMercatorCoord - 经纬度转墨卡托坐标
javascript
const geoMercatorCoord = (longitude, latitude) => {
var E = longitude
var N = latitude
var x = (E * 20037508.34) / 180
var y = Math.log(Math.tan(((90 + N) * Math.PI) / 360)) / (Math.PI / 180)
y = (y * 20037508.34) / 180
return {
x: x, //墨卡托x坐标------对应经度
y: y, //墨卡托y坐标------对应维度
}
}
作用:将地理经纬度坐标转换为墨卡托投影坐标。
参数:
longitude:经度值latitude:纬度值
返回值:包含x、y属性的墨卡托坐标对象
实现原理:使用墨卡托投影公式进行坐标转换,将经度直接线性映射,纬度通过对数函数进行非线性映射,使地图在赤道附近保持比例正确。
geoSphereCoord - 经纬度转球面坐标
javascript
const geoSphereCoord = (R, longitude, latitude) => {
var lon = (longitude * Math.PI) / 180 //转弧度值
var lat = (latitude * Math.PI) / 180 //转弧度值
lon = -lon // three.js坐标系z坐标轴对应经度-90度,而不是90度
// 经纬度坐标转球面坐标计算公式
var x = R * Math.cos(lat) * Math.cos(lon)
var y = R * Math.sin(lat)
var z = R * Math.cos(lat) * Math.sin(lon)
// 返回球面坐标
return {
x: x,
y: y,
z: z,
}
}
作用:将地理经纬度坐标转换为三维球面上的坐标。
参数:
R:球体半径longitude:经度值latitude:纬度值
返回值:包含x、y、z属性的球面坐标对象
实现原理:使用球面坐标转换公式,将经纬度转换为三维空间坐标,适用于创建地球等球面模型。
getBoundingBox - 计算模型包围盒
javascript
const getBoundingBox = group => {
// 包围盒计算模型对象的大小和位置
var box3 = new THREE.Box3()
box3.expandByObject(group) // 计算模型包围盒
var size = new THREE.Vector3()
box3.getSize(size) // 计算包围盒尺寸
var center = new THREE.Vector3()
box3.getCenter(center) // 计算一个层级模型对应包围盒的几何体中心坐标
return {
box3,
center,
size,
}
}
作用:计算3D模型或模型组的包围盒、尺寸和中心坐标。
参数:
group:Three.js模型或模型组对象
返回值:包含包围盒(box3)、中心坐标(center)和尺寸(size)的对象
实现原理:使用Three.js的Box3类计算模型的最小包围立方体,用于后续的相机定位和模型布局。
3D地图建模模块
1. Map3d.js - 地图基类
constructor - 构造函数
javascript
constructor(options = {}) {
let defaultOptions = {
isFull: true,
container: null,
width: window.innerWidth,
height: window.innerHeight,
bgColor: 0x000000,
materialColor: 0xff0000,
controls: {
visibel: true,
enableDamping: true,
autoRotate: false,
maxPolarAngle: Math.PI,
},
statsVisibel: true,
axesVisibel: true,
axesHelperSize: 250,
}
this.options = deepMerge(defaultOptions, options)
this.container = document.querySelector(this.options.container)
this.options.width = this.container.offsetWidth
this.options.height = this.container.offsetHeight
this.scene = new THREE.Scene()
this.camera = null
this.renderer = null
this.mesh = null
this.animationStop = null
this.controls = null
this.stats = null
this.init()
}
作用:初始化地图实例,设置默认参数,创建基本的Three.js场景、相机、渲染器等对象。
参数:
options:地图配置参数对象
执行步骤:
- 合并默认参数和用户参数
- 获取容器元素并设置尺寸
- 初始化Three.js核心对象
- 调用init方法进行进一步初始化
init - 初始化函数
javascript
init() {
this.initStats()
this.initCamera()
this.initModel()
this.initRenderer()
this.initLight()
this.initAxes()
this.initControls()
let gl = this.renderer.domElement.getContext('webgl')
gl && gl.getExtension('WEBGL_lose_context').loseContext()
}
作用:统一调用各个初始化方法,完成地图的全面初始化。
执行步骤:
- 初始化性能统计
- 初始化相机
- 初始化模型(由子类实现)
- 初始化渲染器
- 初始化光源
- 初始化坐标轴辅助
- 初始化控制器
- 释放WebGL上下文(优化内存)
initCamera - 相机初始化
javascript
initCamera() {
let { width, height } = this.options
let rate = width / height
this.camera = new THREE.PerspectiveCamera(45, rate, 0.001, 90000000)
this.camera.up.set(0, 0, 1)
this.camera.position.set(102.97777217804006, 17.660260562607277, 8.029548316292933)
this.camera.lookAt(...centerXY, 0)
}
作用:初始化透视相机,设置相机位置、朝向和视野参数。
执行步骤:
- 计算宽高比
- 创建透视相机实例
- 设置相机上方向(Z轴向上)
- 设置相机位置坐标
- 设置相机看向地图中心点
initRenderer - 渲染器初始化
javascript
initRenderer() {
let { width, height, bgColor } = this.options
let renderer = new THREE.WebGLRenderer({
antialias: true,
})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height)
renderer.setClearColor(bgColor, 1)
this.container.appendChild(renderer.domElement)
this.renderer = renderer
}
作用:初始化WebGL渲染器,设置渲染参数并将渲染画布添加到容器中。
执行步骤:
- 创建WebGL渲染器实例(启用抗锯齿)
- 设置像素比适应高DPI屏幕
- 设置渲染尺寸
- 设置背景颜色
- 将渲染画布添加到DOM容器
initLight - 光源初始化
javascript
initLight() {
// 平行光1
let directionalLight1 = new THREE.DirectionalLight(0x7af4ff, 1)
directionalLight1.position.set(...centerXY, 30)
// 平行光2
let directionalLight2 = new THREE.DirectionalLight(0x7af4ff, 1)
directionalLight2.position.set(...centerXY, 30)
// 环境光
let ambientLight = new THREE.AmbientLight(0x7af4ff, 1)
// 将光源添加到场景中
this.addObject(directionalLight1)
this.addObject(directionalLight2)
this.addObject(ambientLight)
}
作用:初始化场景光源,包括平行光和环境光,增强3D效果。
执行步骤:
- 创建两个平行光并设置位置
- 创建环境光
- 将所有光源添加到场景
initControls - 控制器初始化
javascript
initControls() {
try {
let {
controls: { enableDamping, autoRotate, visibel, maxPolarAngle },
} = this.options
if (!visibel) return false
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.maxPolarAngle = maxPolarAngle
this.controls.autoRotate = autoRotate
this.controls.enableDamping = enableDamping
} catch (error) {
console.log(error)
}
}
作用:初始化轨道控制器,实现地图的交互控制。
执行步骤:
- 检查控制器是否启用
- 创建OrbitControls实例
- 设置控制器参数(最大极角、自动旋转、阻尼效果等)
loop - 渲染循环
javascript
loop() {
this.animationStop = window.requestAnimationFrame(() => {
this.loop()
})
this.renderer.render(this.scene, this.camera)
if (this.options.controls.visibel && this.controls) {
this.controls.update()
}
if (this.options.statsVisibel) this.stats.update()
if (this.rotatingApertureMesh) {
this.rotatingApertureMesh.rotation.z += 0.0005
}
if (this.rotatingPointMesh) {
this.rotatingPointMesh.rotation.z -= 0.0005
}
if (this.css2dRender) {
this.css2dRender.render(this.scene, this.camera)
}
if (this.particleArr.length) {
for (let i = 0; i < this.particleArr.length; i++) {
this.particleArr[i].updateSequenceFrame()
this.particleArr[i].position.z += 0.01
if (this.particleArr[i].position.z >= 6) {
this.particleArr[i].position.z = -6
}
}
}
TWEEN.update()
}
作用:实现地图的持续渲染和动画效果更新。
执行步骤:
- 使用requestAnimationFrame创建渲染循环
- 渲染3D场景
- 更新控制器状态
- 更新性能统计
- 更新旋转光圈动画
- 更新旋转点动画
- 渲染2D标签
- 更新粒子动画
- 更新Tween.js动画
2. App.vue - 自定义地图模型初始化
initModel - 模型初始化(在CurrentMap3d类中重写)
javascript
initModel() {
try {
// 创建组
this.mapGroup = new THREE.Group()
// 标签初始化
this.css2dRender = initCSS2DRender(this.options, this.container)
provinceData.features.forEach((elem, index) => {
// 定一个省份对象
const province = new THREE.Object3D()
// 坐标
const coordinates = elem.geometry.coordinates
// 循环坐标
coordinates.forEach((multiPolygon) => {
multiPolygon.forEach((polygon) => {
const shape = new THREE.Shape()
// 绘制shape
for (let i = 0; i < polygon.length; i++) {
let [x, y] = polygon[i]
if (i === 0) {
shape.moveTo(x, y)
}
shape.lineTo(x, y)
}
// 拉伸设置
const extrudeSettings = {
depth: 0.2,
bevelEnabled: true,
bevelSegments: 1,
bevelThickness: 0.1,
}
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
const mesh = new THREE.Mesh(geometry, [topFaceMaterial, sideMaterial])
province.add(mesh)
})
})
this.mapGroup.add(province)
// 创建标点和标签
initLightPoint(properties, this.mapGroup)
initLabel(properties, this.scene)
})
// 创建上下边框
initBorderLine(provinceData, this.mapGroup)
let earthGroupBound = getBoundingBox(this.mapGroup)
centerXY = [earthGroupBound.center.x, earthGroupBound.center.y]
let { size } = earthGroupBound
let width = size.x < size.y ? size.y + 1 : size.x + 1
// 添加背景,修饰元素
this.rotatingApertureMesh = initRotatingAperture(this.scene, width)
this.rotatingPointMesh = initRotatingPoint(this.scene, width - 2)
initCirclePoint(this.scene, width)
initSceneBg(this.scene, width)
// 将组添加到场景中
this.scene.add(this.mapGroup)
this.particleArr = initParticle(this.scene, earthGroupBound)
initGui()
} catch (error) {
console.log(error)
}
}
作用:初始化3D地图模型,包括省份几何体、材质、标签、装饰元素等。
执行步骤:
- 创建地图模型组
- 初始化2D标签渲染器
- 遍历地图数据创建省份模型
- 为每个省份创建3D几何体和材质
- 添加光柱标记和标签
- 创建地图边框
- 计算地图包围盒和中心点
- 添加装饰元素(旋转光圈、背景等)
- 将地图组添加到场景
- 初始化粒子系统
- 初始化GUI控制器
视觉效果增强模块
1. useMapMarkedLightPillar.js - 光柱标记钩子
createLightPillar - 创建光柱标记
javascript
const createLightPillar = (lon, lat, heightScaleFactor = 1) => {
let group = new THREE.Group()
// 柱体高度
const height = heightScaleFactor
// 柱体的geo,6.19=柱体图片高度/宽度的倍数
const geometry = new THREE.PlaneBufferGeometry(height / 6.219, height)
// 柱体旋转90度,垂直于Y轴
geometry.rotateX(Math.PI / 2)
// 柱体的z轴移动高度一半对齐中心点
geometry.translate(0, 0, height / 2)
// 柱子材质
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(defaultOptions.lightPillarUrl),
color: 0x00ffff,
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
})
// 光柱01
let light01 = new THREE.Mesh(geometry, material)
light01.renderOrder = 99
light01.name = "createLightPillar01"
// 光柱02:复制光柱01
let light02 = light01.clone()
light02.name = "createLightPillar02"
// 光柱02,旋转90°,跟光柱01交叉
light02.rotateZ(Math.PI / 2)
// 创建底部标点
const bottomMesh = createPointMesh()
// 创建光圈
const lightHalo = createLightHalo()
// 将光柱和标点添加到组里
group.add(bottomMesh, lightHalo, light01, light02)
// 设置组对象的姿态
group.position.set(lon, lat, 0)
return group
}
作用:创建包含底部标记、呼吸光圈和交叉光柱的完整标记效果。
参数:
lon:经度坐标lat:纬度坐标heightScaleFactor:光柱高度缩放系数
返回值:包含完整光柱效果的Three.js Group对象
执行步骤:
- 创建光柱组容器
- 计算柱体尺寸和几何体
- 创建柱体贴图材质
- 创建第一个光柱并设置渲染顺序
- 克隆并旋转创建第二个交叉光柱
- 创建底部标记点
- 创建呼吸光圈
- 将所有元素添加到组中
- 设置组的位置坐标
- 返回完整的光柱组
createPointMesh - 创建标记点
javascript
const createPointMesh = () => {
// 标记点:几何体,材质
const geometry = new THREE.PlaneBufferGeometry(1, 1)
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(defaultOptions.pointTextureUrl),
color: 0x00ffff,
side: THREE.DoubleSide,
transparent: true,
depthWrite: false, //禁止写入深度缓冲区数据
})
let mesh = new THREE.Mesh(geometry, material)
mesh.renderOrder = 97
mesh.name = "createPointMesh"
// 缩放
const scale = 0.15 * defaultOptions.scaleFactor
mesh.scale.set(scale, scale, scale)
return mesh
}
作用:创建光柱底部的标记点。
返回值:标记点Mesh对象
实现原理:使用PlaneGeometry创建平面,加载标记点纹理,设置透明和渲染顺序。
createLightHalo - 创建呼吸光圈
javascript
const createLightHalo = () => {
// 标记点:几何体,材质
const geometry = new THREE.PlaneBufferGeometry(1, 1)
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(defaultOptions.lightHaloTextureUrl),
color: 0x00ffff,
side: THREE.DoubleSide,
opacity: 0,
transparent: true,
depthWrite: false, //禁止写入深度缓冲区数据
})
let mesh = new THREE.Mesh(geometry, material)
mesh.renderOrder = 98
mesh.name = "createLightHalo"
// 缩放
const scale = 0.3 * defaultOptions.scaleFactor
mesh.scale.set(scale, scale, scale)
// 动画延迟时间
const delay = random(0, 2000)
// 动画:透明度缩放动画
mesh.tween1 = new TWEEN.Tween({ scale: scale, opacity: 0 })
.to({ scale: scale * 1.5, opacity: 1 }, 1000)
.delay(delay)
.onUpdate((params) => {
let { scale, opacity } = params
mesh.scale.set(scale, scale, scale)
mesh.material.opacity = opacity
})
mesh.tween2 = new TWEEN.Tween({ scale: scale * 1.5, opacity: 1 })
.to({ scale: scale * 2, opacity: 0 }, 1000)
.onUpdate((params) => {
let { scale, opacity } = params
mesh.scale.set(scale, scale, scale)
mesh.material.opacity = opacity
})
mesh.tween1.chain(mesh.tween2)
mesh.tween2.chain(mesh.tween1)
mesh.tween1.start()
return mesh
}
作用:创建带有呼吸动画效果的光圈。
返回值:光圈Mesh对象(带有tween动画)
实现原理:创建平面并加载光圈纹理,使用Tween.js实现透明度和缩放的循环动画,形成呼吸效果。
2. useSequenceFrameAnimate.js - 序列帧动画钩子
createSequenceFrame - 创建序列帧动画
javascript
const createSequenceFrame = ({ image, width, height, frame, column, row, speed = 0.1 }) => {
// 创建平面几何体
const geometry = new THREE.PlaneGeometry(width, height)
// 创建纹理
const texture = new THREE.TextureLoader().load(image)
// 设置纹理参数
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
// 计算每个帧的大小
const frameWidth = 1 / column
const frameHeight = 1 / row
// 设置纹理显示区域
texture.repeat.set(frameWidth, frameHeight)
// 创建材质
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide,
})
// 创建网格
const mesh = new THREE.Mesh(geometry, material)
// 添加动画属性
mesh.currentFrame = 0
mesh.totalFrames = frame
mesh.column = column
mesh.frameWidth = frameWidth
mesh.frameHeight = frameHeight
mesh.speed = speed
mesh.texture = texture
// 添加更新方法
mesh.updateSequenceFrame = function() {
this.currentFrame += this.speed
if (this.currentFrame >= this.totalFrames) {
this.currentFrame = 0
}
const frameIndex = Math.floor(this.currentFrame)
const x = (frameIndex % this.column) * this.frameWidth
const y = 1 - Math.floor(frameIndex / this.column) * this.frameHeight - this.frameHeight
this.texture.offset.set(x, y)
}
return mesh
}
作用:创建基于序列帧图片的动画效果。
参数:
image:序列帧图片路径width:动画宽度height:动画高度frame:总帧数column:每行帧数row:每列帧数speed:动画播放速度
返回值:带有动画更新方法的Three.js Mesh对象
实现原理:通过控制纹理的offset属性,实现序列帧图片的逐帧播放,形成动画效果。
2D标签渲染模块
1. useCSS2DRender.js - CSS2D渲染钩子
initCSS2DRender - 初始化2D渲染器
javascript
const initCSS2DRender = (options, container) => {
const css2dRender = new THREE.CSS2DRenderer()
css2dRender.setSize(options.width, options.height)
css2dRender.domElement.style.position = 'absolute'
css2dRender.domElement.style.top = '0px'
css2dRender.domElement.style.pointerEvents = 'none'
container.appendChild(css2dRender.domElement)
return css2dRender
}
作用:初始化CSS2DRenderer,用于在3D场景中渲染2D HTML元素。
参数:
options:渲染器配置参数container:DOM容器元素
返回值:初始化完成的CSS2DRenderer实例
实现原理:使用Three.js的CSS2DRenderer创建一个与3D渲染器叠加的2D渲染层,用于显示HTML标签。
create2DTag - 创建2D标签
javascript
const create2DTag = (className) => {
const div = document.createElement('div')
div.className = className
div.style.color = '#fff'
div.style.padding = '4px 8px'
div.style.borderRadius = '4px'
div.style.fontSize = '12px'
div.style.whiteSpace = 'nowrap'
div.style.opacity = '0'
const label = new THREE.CSS2DObject(div)
label.visible = false
// 添加显示方法
label.show = function(text, position) {
this.element.innerHTML = text
this.position.copy(position)
this.visible = true
this.element.style.opacity = '1'
}
// 添加隐藏方法
label.hide = function() {
this.visible = false
this.element.style.opacity = '0'
}
return label
}
作用:创建可显示在3D场景中的2D HTML标签。
参数:
className:标签的CSS类名
返回值:带有show和hide方法的CSS2DObject实例
实现原理:创建HTML元素并封装为CSS2DObject,添加显示和隐藏方法,便于在3D场景中控制标签的显示。
地图装饰元素模块
1. App.vue - 装饰元素创建函数
initRotatingAperture - 初始化旋转光圈
javascript
const initRotatingAperture = (scene, width) => {
let plane = new THREE.PlaneBufferGeometry(width, width)
let material = new THREE.MeshBasicMaterial({
map: rotatingApertureTexture,
transparent: true,
opacity: 1,
depthTest: true,
})
let mesh = new THREE.Mesh(plane, material)
mesh.position.set(...centerXY, 0)
mesh.scale.set(1.1, 1.1, 1.1)
scene.add(mesh)
return mesh
}
作用:创建地图底部的旋转光圈效果。
参数:
scene:Three.js场景对象width:光圈宽度
返回值:光圈Mesh对象(在loop函数中更新旋转)
initParticle - 初始化粒子系统
javascript
const initParticle = (scene, bound) => {
// 获取中心点和中间地图大小
let { center, size } = bound
// 构建范围,中间地图的2倍
let minX = center.x - size.x
let maxX = center.x + size.x
let minY = center.y - size.y
let maxY = center.y + size.y
let minZ = -6
let maxZ = 6
let particleArr = []
for (let i = 0; i < 16; i++) {
const particle = createSequenceFrame({
image: "./data/map/上升粒子1.png",
width: 180,
height: 189,
frame: 9,
column: 9,
row: 1,
speed: 0.5,
})
let particleScale = random(5, 10) / 1000
particle.scale.set(particleScale, particleScale, particleScale)
particle.rotation.x = Math.PI / 2
let x = random(minX, maxX)
let y = random(minY, maxY)
let z = random(minZ, maxZ)
particle.position.set(x, y, z)
particleArr.push(particle)
}
scene.add(...particleArr)
return particleArr
}
作用:创建上升粒子效果,增强地图的动态感。
参数:
scene:Three.js场景对象bound:地图边界信息对象
返回值:粒子对象数组(在loop函数中更新位置和动画)
执行步骤:
- 计算粒子生成范围
- 循环创建粒子对象
- 加载序列帧粒子图片
- 设置粒子大小和旋转角度
- 随机分布粒子位置
- 将粒子添加到场景
- 返回粒子数组
总结
本项目通过模块化设计和组件化开发,构建了一个功能丰富、性能优良的3D交互式地图可视化系统。核心函数按照数据处理、3D建模、视觉效果、交互控制等模块进行组织,形成了清晰的调用关系和执行流程。
系统的主要技术亮点包括:
- 高效的数据处理:实现了GeoJSON数据的标准化转换和坐标系统转换
- 精美的3D模型:使用ExtrudeGeometry创建具有立体感的地图模型
- 丰富的视觉效果:包括光柱标记、呼吸光圈、粒子动画等
- 流畅的交互体验:基于OrbitControls实现的相机控制
- 灵活的2D标签:使用CSS2DRenderer实现的3D场景中2D标签渲染
通过对这些核心函数的详细分析,我们可以深入理解3D地图可视化系统的实现原理和技术细节,为类似项目的开发提供参考和借鉴。