likes
comments
collection
share

Threejs 实现3D地图

作者站长头像
站长
· 阅读数 3

现需要使用 Three.js 实现 3D 地图

我们需要思考 如何实现??? (完全没有Threejs 基础)

Q :如果使用 Three.js ,它是渲染出图形的?

A: 参考 Threejs中文网 文档介绍 npm 中 Three的下载路径

Threejs中文网具体地址

npm 中 Three的下载路径

从这个里面我知道 如果需要成功加载一个3D图形,我需要 5步走

  1. 创建环境
  2. 创建相机
  3. 创建渲染器
  4. 创建物体
  5. 渲染场景
//引入
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: 我们本质上只需要改变它的物体就可以了,让他变成地图,展示出来

这里地图的 几何体我们该用什么? 地图的材质要用什么? 地图的数据该从哪里来?如何才能展现出来?

几何体有哪些?

Threejs 实现3D地图

材质有那些?

Threejs 实现3D地图

地图的几何体用 : 挤压缓冲几何体(ExtrudeGeometry) 参考示例 基础线条材质(LineBasicMaterial) 参考示例 用来单独描线的 也可以不用 ,根据需求来 地图的材质用: 基础网格材质(MeshBasicMaterial) 介绍 示例:

/*
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小工具 获取到

Threejs 实现3D地图

数据拿到之后,就是展示的问题,直接展示是不行的,需要通过 d3 对数据处理,才能按照正确的地图样子展示

import * as d3 from "d3";  //莫开托坐标 矫正地图坐标
//center 的位置可以自己定
const handleProj = d3.geoMercator().center([109, 34.5]).scale(1000).translate([0, 0]) // d3投影转换函数

接下来就是封装地图信息 ,使它变成这个样子 (这里用到了)

 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 );
/*
	'/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)

Threejs 实现3D地图

以上到这里 ,一个不能动的地图出现了!

tip: 如果有不展示的 可以更改相机的缩放,d3投影的缩放,加点环境光(下面都是我修改和添加了的)

// 这里的 都是修改过的
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 中的控件时需要 引入的


import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true  //阻尼 更真实

这个是动态的地图 Threejs 实现3D地图

贴上全部代码,有些可以自行删减

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