Three.js制作三维地图(一)

待解决的问题

  1. 在绘制地图之前,所有的定位数据都是经纬度数据,但是绘制需要将经纬度数据转化为三维世界的坐标系,所以涉及到墨卡托投影,将经纬度转化为三维坐标,再根据经纬度进行定位绘制各种图形。
  2. 项目需求是要分级展示地图,包括全国级别,区域级别,省级,市级。地图边缘数据从echarts开源数据可以获得,但如何解密并解析为我们所需的格式。
  3. 在频繁的层级切换中,如何应对浏览器卡顿的问题,涉及到模型的销毁与重绘、清空一些不必要的材质等。
  4. 绘制飞线和风电机,模型的旋转、属性的更改、模型的点击事件、动画和节流问题。

初始化场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
function init(){
// 初始化场景
scene = new THREE.Scene();
scene.background = new THREE.Color( "#ccc" );

// 设置天空盒 front,back上下,left,right 左右 top bootom 前后
var path = "../assets/webglmap/images/Textures/";
var format = '.jpg';
var urls = [
path + 'PurpleNebula2048_left' + format, path + 'PurpleNebula2048_right' + format,
path + 'PurpleNebula2048_top' + format, path + 'PurpleNebula2048_bottom' + format,
path + 'PurpleNebula2048_front' + format, path + 'PurpleNebula2048_back' + format
]

var textureCube = new THREE.CubeTextureLoader().load( urls )

scene = new THREE.Scene()
scene.background = textureCube

// 设置渲染器
renderer = new THREE.WebGLRenderer({ antialias:true })
renderer.setSize( window.innerWidth, window.innerHeight )
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
document.body.appendChild( renderer.domElement )

// 设置相机
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 1000 )
camera.up.x = 0
camera.up.y = 0
camera.up.z = 1
camera.position.set(60, 0, 120 )
camera.lookAt( -20, 0, 100 )

// 设置半球光
var light = new THREE.HemisphereLight( 0xffffff, 0x444444 )
light.position.set( 0, 0, 200 )
scene.add( light )

// 设置平行光
dlight = new THREE.DirectionalLight( 0xffffff, 1 )
dlight.position.set( 3, 70, 100 )
scene.add( dlight );

// 设置控制器
controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.maxPolarAngle = Math.PI/2
controls.minDistance = 0
controls.maxDistance = 700
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;

// 设置坐标轴辅助线
axesHelper = new THREE.AxesHelper( 1000 );
scene.add( axesHelper );

// 设置光线投射
raycaster = new THREE.Raycaster();

// 设置鼠标坐标
mouse = new THREE.Vector2();
}

// 浏览器窗口变动触发的方法
function onWindowResize() {
// 重新设置相机宽高比例
camera.aspect = window.innerWidth / window.innerHeight;
// 更新相机投影矩阵
camera.updateProjectionMatrix();
// 重新设置渲染器渲染范围
renderer.setSize(window.innerWidth, window.innerHeight);
render()
}

// 添加window 的resize事件监听
window.addEventListener('resize',onWindowResize);

读取地图边缘数据

中国地图边缘数据为后端提供,也可读echarts开源地图数据绘制,需要解密和数据解析。(后文解释如何解密和数据解析)下面为计算中国地图边缘三维数据,将经纬度坐标做墨卡托投影,转换为三维坐标。

1
2
3
4
5
6
7
8
9
10
// 计算中国地图边缘三维数据
function get_china_vector3EdgeJson(){
for(i in china_edgeJson){
var area = []
for(j in china_edgeJson[i]){
area.push(lnglatToMercator(china_edgeJson[i][j]))
}
china_vector3EdgeJson.push(area)
}
}

以下为墨卡托转化方法,使用d3提供的墨卡托转换方法。
d3.geo

1
2
3
4
5
6
7
8
9
10
11
// 设置墨卡托转换中心点和缩放范围以及偏移位置
projection = d3.geoMercator().center([123.307279,46.375662]).scale(150).translate([-6,10])

// 墨卡托转化
function lnglatToMercator(edgepoint){
var p = []
var edgepoint = [edgepoint.lng,edgepoint.lat]
var point = projection(edgepoint)
p.push(point[1],point[0],0)
return p
}

绘制中国地图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 绘制中国地图
function draw_chinaMap(){
chinaMapGroup = new THREE.Group()
for(var i = 0; i < china_vector3EdgeJson.length; i++){
var shape = new THREE.Shape()
for(var j = 0; j < china_vector3EdgeJson[i].length-1; j++){
if(j === 0){
shape.moveTo(china_vector3EdgeJson[i][j][0],china_vector3EdgeJson[i][j][1])
}else if(j === china_vector3EdgeJson[i].length-2){
shape.quadraticCurveTo( china_vector3EdgeJson[i][j][0],china_vector3EdgeJson[i][j][1],china_vector3EdgeJson[i][j][0],china_vector3EdgeJson[i][j][1] )
}else{
shape.lineTo(china_vector3EdgeJson[i][j][0],china_vector3EdgeJson[i][j][1])
}
}
var extrudeSettings = { amount: h, bevelEnabled: false, bevelSegments: 2, steps: 2, bevelSize: 1, bevelThickness: 1 };
var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings )
var material = new THREE.MeshStandardMaterial( { color: mapcolor, transparent: true, opacity: mapopacity} );
var mesh = new THREE.Mesh( geometry, material )
mesh.castShadow = true
mesh.receiveShadow = true
chinaMapGroup.add(mesh)
}
scene.add(chinaMapGroup)
}

绘制区域文字

在对应的区域位置绘制文字模型,并为文字绑定鼠标滑动高亮和点击跳转到省级别事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 六大区域名称
var areas = [
{ name:'东北', cp:{'lng':123.307279,'lat':46.375662} },
{ name:'华北', cp:{'lng':115.433236,'lat':41.707946} },
{ name:'西北', cp:{'lng':96.888761,'lat':39.120975} },
{ name:'西南', cp:{'lng':98.80208,'lat':31.2172815} },
{ name:'华中', cp:{'lng':113.814274,'lat':29.236317} },
{ name:'华东', cp:{'lng':118.523982,'lat':31.784774} }
]

// 绘制六大区域名称
function drawAreasText(){
areasTextgroup = new THREE.Group()
areas.forEach(area => {
var point = area.cp
var p = lnglatToMercator(point)
var loader = new THREE.FontLoader()
loader.load( '../assets/webglmap/font/SimHei_Regular.json', function ( font ) {
var text
var matLite = new THREE.MeshBasicMaterial( {
color: "#fff",
transparent: true,
opacity: 1,
side: THREE.DoubleSide
} )
var shapes = font.generateShapes( area.name, 2 )
// var geometry = new THREE.ShapeBufferGeometry( shapes )
// text = new THREE.Mesh( geometry, matLite )
var extrudeSettings = { amount: 0.5, bevelEnabled: false, bevelSegments: 2, steps: 2, bevelSize: 1, bevelThickness: 1 };
var geometry = new THREE.ExtrudeGeometry( shapes, extrudeSettings )
var material = new THREE.MeshStandardMaterial( { color: "#fff", transparent: true, opacity: 1} );
var text = new THREE.Mesh( geometry, material )
text.castShadow = true
text.receiveShadow = true
text.position.x = p[0]
text.position.y = p[1]
text.position.z = 6.2
// text.rotation.x = Math.PI/2
// text.rotation.y = Math.PI/2
text.rotation.z = Math.PI/2
text.name = area.name
areasClickObjects.push(text)
areasTextgroup.add(text)
})
scene.add(areasTextgroup)
})
}

// 鼠标滑过六大区域名高亮
function areasMouseEvent(event){
//点击射线
event.preventDefault()
mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1
mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1
// 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera( mouse, camera )
// 计算物体和射线的焦点
intersects = raycaster.intersectObjects( areasClickObjects )
areasClickObjects.forEach((mesh)=>{
mesh.material.color.set( "#fff" )
})
intersects.forEach((mesh)=>{
mesh.object.material.color.set( "#ffff00" )
})
}

// 点击六大区域名称
function areasClickEvent(event) {
event.preventDefault();
mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1
mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1
raycaster.setFromCamera(mouse, camera);
// 这里必须装网格,mesh,装入组是没有效果的
// 所以我们将所有的盒子的网格放入对象就可以了
// 需要被监听的对象要存储在clickObjects中。
if(areasClickObjects.length!=0){
var intersects = raycaster.intersectObjects(areasClickObjects)
if(intersects.length > 0) {
// 制定跳转代码
areaLevelName = intersects[0].object.name
}
}
}

绘制电厂

根据后端提供的包含站点经纬度、站点名称、站点id、站点电压等级的json数据绘制电厂,使用圆柱绘制,根据电压等级绘制不同颜色的电厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 绘制全国拓扑站点
function draw_chinaTPLocation(){
chinaTPLocationnGroup.children = []
for(var i = 0; i < china_vector3TPLocationJson.length; i++){
if(china_vector3TPLocationJson[i].type == '110kV'){
stationcolor = "#7bca62"
}else if(china_vector3TPLocationJson[i].type == '220kV'){
stationcolor = "#03a9f4"
}else if(china_vector3TPLocationJson[i].type == '500kV'){
stationcolor = "#9c27b0"
}else if(china_vector3TPLocationJson[i].type == '800kV'){
stationcolor = "#9c27b0"
}else if(china_vector3TPLocationJson[i].type == '1000kV'){
stationcolor = "#f39000"
}
var geometry = new THREE.CylinderBufferGeometry( 0.3, 0.3, 4, 10 )
var material = new THREE.MeshStandardMaterial( {color: stationcolor, transparent: true, opacity: 0.8} )
var cylinder = new THREE.Mesh( geometry, material )
cylinder.rotation.z = Math.PI/2
cylinder.rotation.y = Math.PI/2
cylinder.name = china_vector3TPLocationJson[i].id
cylinder.position.x = china_vector3TPLocationJson[i].position[0]
cylinder.position.y = china_vector3TPLocationJson[i].position[1]
cylinder.position.z = 0
chinaTPLocationnGroup.add(cylinder)
}
scene.add(chinaTPLocationnGroup)
}

绘制线路

绘制三维空间飞线,具备电流流向的效果,使用更改线段颜色属性的原理,使用动画+节流绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 绘制全国拓扑线路
function draw_chinaTPLineJson(){
chinaTPLineGroup.children = []
china_vector3TPLineJson.forEach((line)=>{
if(line.type == 1){
linecolor = "#f39000"
}else if(line.type == 2){
linecolor = "#7bca62"
}else if(line.type == 3){
linecolor = "#03a9f4"
}else if(line.type == 4){
linecolor = "#9c27b0"
}else if(line.type == 5){
linecolor = "#443366"
}
var curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3( line.source[0], line.source[1], h+0.4 ),
new THREE.Vector3( (line.source[0]+line.target[0])/2, (line.source[1]+line.target[1])/2, h+0.4 ),
new THREE.Vector3( line.target[0], line.target[1], h+0.4 )
)
var points = curve.getPoints( 40 )
var geometry = new THREE.Geometry()
geometry.vertices = points
geometry.colors = new Array(points.length).fill(new THREE.Color(linecolor))
var material = new THREE.LineBasicMaterial( {
vertexColors: THREE.VertexColors,
transparent: true,
opacity: 1
} )
var curveObject = new THREE.Line( geometry, material )
curveObject.name = line.name
chinaTPLineGroup.add(curveObject)

})
scene.add(chinaTPLineGroup)
}

// 更新全国双态拓扑线路
update_doublechina_vector3TPLineJson = _.throttle(() => {
if(doublechinaTPLineGroup!=undefined){
doublechinaTPLineGroup.children.forEach(line=>{
for(i in china_vector3TPLineJson){
if(china_vector3TPLineJson[i].name == line.name){
if(china_vector3TPLineJson[i].type == 1){
linecolor = "#f39000"
}else if(china_vector3TPLineJson[i].type == 2){
linecolor = "#7bca62"
}else if(china_vector3TPLineJson[i].type == 3){
linecolor = "#03a9f4"
}else if(china_vector3TPLineJson[i].type == 4){
linecolor = "#9c27b0"
}else if(china_vector3TPLineJson[i].type == 5){
linecolor = "#443366"
}
line.geometry.colors[0] = linecolor
}
}
})
}
},1000)

书籍资源

电子书《WebGL零基础入门教程》
电子书《Three.js教程》
Three.js中文文档