Threejs 实现3D地图

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

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

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

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

Threejs中文网具体地址

npm 中 Three的下载路径

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

  1. 创建环境
  2. 创建相机
  3. 创建渲染器
  4. 创建物体
  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>
相关推荐
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
吹牛不交税10 小时前
admin.net-v2 框架使用笔记-netcore8.0/10.0版
vue.js·.netcore