现需要使用 Three.js 实现 3D 地图
我们需要思考 如何实现??? (完全没有Threejs 基础)
Q :如果使用 Three.js ,它是渲染出图形的?
A: 参考 Threejs中文网 文档介绍 npm 中 Three的下载路径
从这个里面我知道 如果需要成功加载一个3D图形,我需要 5步走
- 创建环境
- 创建相机
- 创建渲染器
- 创建物体
- 渲染场景
javascript
//引入
import * as THREE from 'three';
const width = window.innerWidth, height = window.innerHeight;
// 1.创建一个场景
const scene = new THREE.Scene();
// 2.创建一个相机 PerspectiveCamera(透视摄像机)。
const camera = new THREE.PerspectiveCamera( 70, width / height, 0.01, 10 );
camera.position.z = 1; //向z轴偏移
// 3.创建渲染器
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( width, height ); //渲染多大 要尺寸
//4.创建一个物体 构造器 Mesh 由 几何体+材质构成
const geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 ); //BoxGeometry 立方缓冲几何体
const material = new THREE.MeshNormalMaterial(); //材质 基础网格材质
const mesh = new THREE.Mesh( geometry, material ); // 构造器
scene.add( mesh );
//5.渲染场景
renderer.setAnimationLoop( animation );
document.body.appendChild( renderer.domElement ); //添加到页面中
// animation
function animation( time ) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render( scene, camera );
}
Q :我们已经解决了 Three 是如何展示3D图形的,那么实现3D地图,我需要那些必备的属性?
A: 我们本质上只需要改变它的物体就可以了,让他变成地图,展示出来
这里地图的 几何体我们该用什么? 地图的材质要用什么? 地图的数据该从哪里来?如何才能展现出来?
几何体有哪些?
材质有那些?
地图的几何体用 : 挤压缓冲几何体(ExtrudeGeometry) 参考示例 基础线条材质(LineBasicMaterial) 参考示例 用来单独描线的 也可以不用 ,根据需求来 地图的材质用: 基础网格材质(MeshBasicMaterial) 介绍 示例:
javascript
/*
ExtrudeGeometry(shapes : Array, options : Object)
shapes --- 形状或者一个包含形状的数组。
options --- 一个包含有下列参数的对象:
curveSegments --- int,曲线上点的数量,默认值是12。
steps --- int,用于沿着挤出样条的深度细分的点的数量,默认值为1。
depth --- float,挤出的形状的深度,默认值为1。
bevelEnabled --- bool,对挤出的形状应用是否斜角,默认值为true。
bevelThickness --- float,设置原始形状上斜角的厚度。默认值为0.2。
bevelSize --- float。斜角与原始形状轮廓之间的延伸距离,默认值为bevelThickness-0.1。
bevelOffset --- float. Distance from the shape outline that the bevel starts. Default is 0.
bevelSegments --- int。斜角的分段层数,默认值为3。
extrudePath --- THREE.Curve对象。一条沿着被挤出形状的三维样条线。Bevels not supported for path extrusion.
UVGenerator --- Object。提供了UV生成器函数的对象。
该对象将一个二维形状挤出为一个三维几何体。
当使用这个几何体创建Mesh的时候,如果你希望分别对它的表面和它挤出的侧面使用单独的材质,你可以使用一个材质数组。 第一个材质将用于其表面;第二个材质则将用于其挤压出的侧面。
属性
*/
const length = 12, width = 8;
const shape = new THREE.Shape(); //形状
shape.moveTo( 0,0 );
shape.lineTo( 0, width );
shape.lineTo( length, width );
shape.lineTo( length, 0 );
shape.lineTo( 0, 0 );
const extrudeSettings = {
steps: 2,
depth: 16,
bevelEnabled: true,
bevelThickness: 1,
bevelSize: 1,
bevelOffset: 0,
bevelSegments: 1
};
const geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const mesh = new THREE.Mesh( geometry, material ) ;
scene.add( mesh );
由此 我们知道了,地图的是通过这个 shape 描绘成形状展示的,数据可以通过 datav.aliyun 地图json小工具 获取到
数据拿到之后,就是展示的问题,直接展示是不行的,需要通过 d3 对数据处理,才能按照正确的地图样子展示
javascript
import * as d3 from "d3"; //莫开托坐标 矫正地图坐标
//center 的位置可以自己定
const handleProj = d3.geoMercator().center([109, 34.5]).scale(1000).translate([0, 0]) // d3投影转换函数
接下来就是封装地图信息 ,使它变成这个样子 (这里用到了)
javascript
const shape = new THREE.Shape(); //形状
shape.moveTo( 0,0 );
shape.lineTo( 0, width );
shape.lineTo( length, width );
shape.lineTo( length, 0 );
shape.lineTo( 0, 0 );
javascript
/*
'/src/assets/map/map.json' 是在src目录下自己创建的,
map.json 是通过 datav这个地图小工具下载的
*/
import * as d3 from "d3"; //莫开托坐标 矫正地图坐标
import map from '../assets/map/map.json'
const handleProj = d3.geoMercator().center([109, 34.5]).scale(1000).translate([0, 0]) // d3投影转换函数
const mapContainer = new THREE.Object3D() // 存储地图Object3D对象
// 处理地图数据 GeoJson data
const handleData = (jsonData) => {
const feaureList = jsonData.features;
feaureList.forEach((feature) => { // 每个feature都代表一个省份
const province = new THREE.Object3D;
province.properties = feature.properties.name // 省份名称
province.name = feature.properties.name // 省份名称
mapContainer.name = feature.properties.name // 省份名称
const coordinates = feature.geometry.coordinates // 省份坐标信息
// 处理的原因可以自己打印map.json 看
if (feature.geometry.type === 'MultiPolygon') {
coordinates.forEach((coord) => {
coord.forEach((coordinate) => {
// 三维多边形
const extrudeMesh = creatDepthPolygon(coordinate)
extrudeMesh.properties = feature.properties.name
// 线条
const line = createLine(coordinate);
province.add(extrudeMesh)
province.add(line)
})
})
}
if (feature.geometry.type === 'Polygon') {
coordinates.forEach((coordinate) => {
// 三维多边形
const extrudeMesh = creatDepthPolygon(coordinate)
extrudeMesh.properties = feature.properties.name
// 线条
const line = createLine(coordinate);
province.add(extrudeMesh)
province.add(line)
})
}
mapContainer.add(province)
})
scene.add(mapContainer)
}
// 创建三维多边形
const creatDepthPolygon = (coordinate) => {
const shape = new THREE.Shape();
coordinate.forEach((item, index) => { // 每一个item都是MultiPolygon中的一个polygon
const [x_XYZ, y_XYZ] = handleProj(item)
if (index === 0) {
shape.moveTo(x_XYZ, -y_XYZ)
} else {
shape.lineTo(x_XYZ, -y_XYZ)
}
})
const extrudeSettings = {
steps: 2,
depth: 16,
bevelEnabled: true,
bevelThickness: 1,
bevelSize: 1,
bevelOffset: 0,
bevelSegments: 1
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings) //挤压缓冲几何体
const material = new THREE.MeshBasicMaterial({
// color: new THREE.Color(Math.random() * 0xffffff), // 每个省随机赋色
color: '#d13a34',
transparent: true,
opacity: 0.6
})
return new THREE.Mesh(geometry, material)
}
// 创建线条
const createLine = (coordinate) => {
const material = new THREE.LineBasicMaterial({
color: '#ffffff'
});
const points = []
coordinate.forEach((item, index) => { // 每一个item都是MultiPolygon中的一个polygon
const [x_XYZ, y_XYZ] = handleProj(item)
points.push(new THREE.Vector3(x_XYZ, -y_XYZ, 25))
})
const geometry = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geometry, material);
}
//调用
handleData(map)
以上到这里 ,一个不能动的地图出现了!
tip: 如果有不展示的 可以更改相机的缩放,d3投影的缩放,加点环境光(下面都是我修改和添加了的)
javascript
// 这里的 都是修改过的
const handleProj = d3.geoMercator().center([109, 34.5]).scale(1000).translate([0, 0]) // d3投影转换函数
const mapContainer = new THREE.Object3D() // 存储地图Object3D对象
// 创建相机
const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10000);
camera.position.z = 1000;
// 创建3D场景对象Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff'); // 将背景颜色设置为白色
// 初始化环境光
const initLight = () => {
const ambLight = new THREE.AmbientLight('#ffffff', 0.3) // 基本光源
const spotLight = new THREE.SpotLight(0xFFFFFF); // 聚光灯
spotLight.position.set(40, 200, 10);
spotLight.castShadow = true; // 只有该属性为true时,该点光源允许产生阴影,并且下列属性可用
scene.add(ambLight, spotLight); // 向场景中添加光源
}
Q:如果要想动起来呢?? 鼠标滑动也能愉快的转圈圈
A:需要用到控件
设置相机控件轨道控制器OrbitControls 相机控件轨道控制器
注意!!! Threejs 中的控件时需要 引入的
javascript
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true //阻尼 更真实
这个是动态的地图
贴上全部代码,有些可以自行删减
javascript
<template>
<div id="info"></div>
</template>
<script setup>
import { onMounted,ref } from 'vue'
import * as THREE from 'three'
import * as d3 from "d3"; //莫开托坐标 矫正地图坐标
import map from '../assets/map/map.json'
// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 文本缓冲几何体
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
// 一个用于加载JSON格式的字体的类
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
const width = window.innerWidth, height = window.innerHeight;
const handleProj = d3.geoMercator().center([109, 34.5]).scale(1000).translate([0, 0]) // d3投影转换函数
const mapContainer = new THREE.Object3D() // 存储地图Object3D对象
// 创建相机
const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10000);
camera.position.z = 1000;
// 创建3D场景对象Scene
const scene = new THREE.Scene();
scene.background = new THREE.Color('#ffffff'); // 将背景颜色设置为白色
// 初始化环境光
const initLight = () => {
const ambLight = new THREE.AmbientLight('#ffffff', 0.3) // 基本光源
/**
* 设置聚光灯相关的的属性,详情见P54
*/
const spotLight = new THREE.SpotLight(0xFFFFFF); // 聚光灯
spotLight.position.set(40, 200, 10);
spotLight.castShadow = true; // 只有该属性为true时,该点光源允许产生阴影,并且下列属性可用
scene.add(ambLight, spotLight); // 向场景中添加光源
}
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
// 初始化地理数据集
const initGeom = () => {
// 加载中国地区的geoJson数据集
// const fileLoader = new THREE.FileLoader();
// fileLoader.load('/src/assets/map/map.json',
// (data) => {
// const chinaJson = JSON.parse(data)
// handleData(chinaJson)
// }
// )
handleData(map)
}
// 处理地图数据 GeoJson data
const handleData = (jsonData) => {
const feaureList = jsonData.features;
feaureList.forEach((feature) => { // 每个feature都代表一个省份
const province = new THREE.Object3D;
province.properties = feature.properties.name // 省份名称
province.name = feature.properties.name // 省份名称
mapContainer.name = feature.properties.name // 省份名称
const coordinates = feature.geometry.coordinates // 省份坐标信息
if (feature.geometry.type === 'MultiPolygon') {
coordinates.forEach((coord) => {
coord.forEach((coordinate) => {
// 三维多边形
const extrudeMesh = creatDepthPolygon(coordinate)
extrudeMesh.properties = feature.properties.name
// 线条
const line = createLine(coordinate);
province.add(extrudeMesh)
province.add(line)
})
})
}
if (feature.geometry.type === 'Polygon') {
coordinates.forEach((coordinate) => {
// 三维多边形
const extrudeMesh = creatDepthPolygon(coordinate)
extrudeMesh.properties = feature.properties.name
// 线条
const line = createLine(coordinate);
province.add(extrudeMesh)
province.add(line)
})
}
mapContainer.add(province)
})
scene.add(mapContainer)
}
// 创建三维多边形
const creatDepthPolygon = (coordinate) => {
const shape = new THREE.Shape();
coordinate.forEach((item, index) => { // 每一个item都是MultiPolygon中的一个polygon
const [x_XYZ, y_XYZ] = handleProj(item)
if (index === 0) {
shape.moveTo(x_XYZ, -y_XYZ)
} else {
shape.lineTo(x_XYZ, -y_XYZ)
}
})
const extrudeSettings = {
steps: 2,
depth: 16,
bevelEnabled: true,
bevelThickness: 1,
bevelSize: 1,
bevelOffset: 0,
bevelSegments: 1
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings) //挤压缓冲几何体
const material = new THREE.MeshBasicMaterial({
// color: new THREE.Color(Math.random() * 0xffffff), // 每个省随机赋色
color: '#d13a34',
transparent: true,
opacity: 0.6
})
return new THREE.Mesh(geometry, material)
}
// 创建线条
const createLine = (coordinate) => {
const material = new THREE.LineBasicMaterial({
color: '#ffffff'
});
const points = []
coordinate.forEach((item, index) => { // 每一个item都是MultiPolygon中的一个polygon
const [x_XYZ, y_XYZ] = handleProj(item)
points.push(new THREE.Vector3(x_XYZ, -y_XYZ, 25))
})
const geometry = new THREE.BufferGeometry().setFromPoints(points);
return new THREE.Line(geometry, material);
}
// 光线投射Raycaster
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
//鼠标放上去 改变颜色 显示地区名字
let activeIntersects = []; //鼠标滑过数据
const onPointerMove = (event) => {
let info = document.querySelector('#info')
// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera(pointer, camera);
// 判断数组是否有数据,有数据全部设置为原始数据
if (activeIntersects.length) {
for (let i = 0; i < activeIntersects.length; i++) {
activeIntersects[i].object.material.color.set('#d13a34');
}
}
// 计算物体和射线的焦点
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length && intersects[0].object.parent.name) {
// 设置hove 弹框的宽高
info.style.left = event.clientX + 'px'
info.style.top = event.clientY + 'px'
info.style.display = 'block'
info.innerHTML = intersects[0].object.parent.name
}else{
info.style.display = 'none'
}
// 数组数据清空
activeIntersects = []
// 滑过的当前这个高亮
for (let i = 0; i < intersects.length; i++) {
if (intersects[i].object.type === 'Mesh') {
intersects[i].object.material.color.set(0xff0000);
activeIntersects.push(intersects[i])
}
}
}
window.addEventListener('pointermove', onPointerMove);
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true //阻尼 更真实
// // 辅助线 AxesHelper
// const axesHelper = new THREE.AxesHelper( 500 );
// scene.add( axesHelper );
// // Three.js 中绘制标签信息 地图名称
// // 创建省份名称标签
// var loader = new FontLoader();
// loader.load('/src/assets/fonts/helvetiker_regular.typeface.json', function (font) {
// const geometry = new TextGeometry('mapContainer.name ', {
// font: font,
// size: 80,
// height: 5,
// curveSegments: 12,
// bevelEnabled: true,
// bevelThickness: 10,
// bevelSize: 8,
// bevelSegments: 5
// } );
// const textMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// const textMesh = new THREE.Mesh(geometry, textMaterial);
// textMesh.position.x = -1;
// textMesh.position.y = 1;
// scene.add(textMesh);
// });
// 渲染
// 因为后期是每一帧都需要渲染,需要封装一个渲染函数
const render = () => {
// 使用渲染器,通过相机 将场景渲染出来
renderer.render(scene, camera)
// 渲染下一帧的时候会调用render函数
requestAnimationFrame(render)
}
// 4.获取dom实例
onMounted(() => {
initGeom();
initLight();
render()
document.body.appendChild(renderer.domElement);
})
</script>
<style>
#info {
position: absolute;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border-radius: 2px;
padding: 5px 10px;
display: none;
width: auto; /* 设置宽度自适应 */
}
</style>