前言
本文是一次尝试性的创新,代码是作者写的,但是下面的文章内容是把代码扔给AI,让AI写的,作者整理的,感觉比作者之前的文章写的细致,但是有点太细了,不知各位看官是否习惯这样的文章,欢迎大家提意见。五万多个字呢,比以往所有的内容加起来都多
效果图

线上演示地址,点击体验
源码下载地址,点击下载
视频演示
局部动图
文件目录
📁content
│ │ ├─ 📄GUI.ts-------------------控制器
│ │ ├─ 📄constData.ts-------------告警信息坐标
│ │ ├─ 📄grid.ts------------------网格辅助线
│ │ ├─ 📄light.ts-----------------灯光
│ │ ├─ 📄main.ts------------------入口文件
│ │ ├─ 📄map.ts-------------------绘制线条形状
│ │ ├─ 📄materials.ts-------------材质
│ │ ├─ 📄request.ts---------------线条形状点位请求
│ │ ├─ 📄scene.ts-----------------场景必要元素
│ │ ├─ 📄tag.ts-------------------告警标签
│ │ ├─ 📄tagLine.ts---------------告警标签连接线
│ │ ├─ 📄tagPanel.ts--------------告警面板
│ │ └─ 📄tornado.ts---------------粒子运动(龙卷风)
│ ├─ 📁utils ----------------------工具库
│ │ ├─ 📄IntervalTime.ts----------定时器class类
│ │ ├─ 📄index.ts-----------------常用工具
│ │ └─ 📄unreal.ts----------------场景发光
技术栈
- “three”: “0.167.0”,
- “typescript”: “^5.0.2”,
- “vite”: “^4.3.2”
正文
辅助线
创建坐标格辅助线,当你的场景背景太单调时,可以用做于背景图,接受四个参数,很简单,官网有详细案例。grid.userData.isLight = true
用于标记是否接受发光场景的影响,如不需要发光,可不进行标记,后续需要发光的元素都会进行标记,不在后文赘述。
const grid = new THREE.GridHelper(200, 500, 0x1c252d, 0x1c252d);
grid.userData.isLight = true
scene.add(grid);
形状及线条
https://geo.datav.aliyun.com/areas_v3/bound/330000_full.json
从该网站获取到形状的顶点信息,过滤编号为330100
的形状,因为这个形状要做挤压缓冲几何体,也是为了减少浏览器的渲染压力,接口返回的顶点信息为2d。x和y的坐标,所以需要处理一下,将顶点信息改为x和z,y设置为0。
绘制线条
- 请求数据并进行绘制
// 获取杭州市区地图数据
fetchHZJSapData().then((data) => {
const features = data.features
features.forEach((feature: any) => {
const arcs = feature.geometry.coordinates[0][0];
// 排除adcode为330100
if (feature.properties.adcode !== 330100) {
const positions = getPositions(arcs)
// 创建较暗的边界线
const line = createLine(positions, 0x323748, .8);
cityGroup.add(line)
}
})
})
首先调用fetchHZJSapData()
函数来获取数据,该函数返回一个Promise
。当Promise
成功 resolve 后,会将获取到的数据传入后续的回调函数中进行处理。
在回调函数中,从获取到的数据对象中提取出features
属性值,并将其赋值给features
变量,以便后续对每个特征数据进行遍历操作。
处理数据getPositions
方法
// 将二维坐标转换为三维坐标数组
export const getPositions = (arcs: number[][]) => {
let positions: number[] = [];
arcs.forEach((v2Arr: number[]) => {
const x = v2Arr[0];
const z = v2Arr[1];
positions.push(x, 0, z); // y坐标设为0
});
return positions;
}
创建Line2,并添加到组中createLine
首先创建线条几何体(LineGeometry
)对象,通过调用new LineGeometry()
完成,该对象用于定义线条的几何形状,随后使用geometry.setPositions(positions)
设置线条的顶点位置,即将传入的顶点坐标数组应用到几何体上。
接着创建线条对象,通过new Line2(geometry, lineMaterial(color, opacity, width, dashed))
来实现,其中Line2
是用于渲染线条的类,而lineMaterial
函数用于生成线条的材质,材质的属性由传入的color
、opacity
、width
和dashed
等参数确定。
如果dashed
参数为true
,即线条是虚线的情况下,会调用line.computeLineDistances()
来计算线段的距离,以实现虚线效果。
// 创建线条对象
/**
* 创建线条对象
* @param positions 顶点坐标数组
* @param color 线条颜色
* @param opacity 线条透明度
* @param width 线条宽度
* @param isLight 是否为光源
* @param dashed 是否为虚线
* @returns
*/
export function createLine(positions: number[], color: number, opacity = 1, width = 1, isLight = false, dashed = false) {
// 创建线条几何体
const geometry = new LineGeometry(); // LineGeometry用于定义线条的几何形状
geometry.setPositions(positions); // 设置线条的顶点位置,positions是一个包含坐标的数组
// 创建线条对象,使用lineMaterial函数生成材质
const line = new Line2(geometry, lineMaterial(color, opacity, width, dashed)); // Line2是用于渲染线条的类
line.userData.isLight = isLight; // 将isLight属性存储在userData中,方便后续使用
if (dashed) {
// 如果线条是虚线
line.computeLineDistances(); // 计算线段的距离,用于虚线效果
}
return line; // 返回创建的线条对象
}
线条材质
通过调用new LineMaterial({...})
创建一个线条材质对象,传入一个包含多个属性的配置对象。
在配置对象中,设置了以下属性:
color
:传入的线条颜色值。linewidth
:传入的线条宽度值。opacity
:传入的线条透明度值。transparent
:设置为true
,表示线条是透明的,结合opacity
属性可以实现不同程度的透明效果。vertexColors
:设置为false
,表示不使用顶点颜色,可能是用于控制线条颜色的一种方式(具体效果取决于渲染引擎的实现)。dashed
:传入的布尔值,决定线条是否为虚线。dashSize
:当线条为虚线时,设置虚线的线段长度。gapSize
:当线条为虚线时,设置虚线的间隔长度。
函数返回值
最后,函数返回创建好的LineMaterial
对象,这个对象可以在创建线条对象时被使用,以设置线条的材质属性,从而实现特定的线条渲染效果。
export const lineMaterial = (color: number, opacity: number, width = 1,dashed=false) => {
const material = new LineMaterial({
color,
linewidth: width,
opacity,
transparent: true,
vertexColors: false,
dashed,
dashSize: 0.05,
gapSize: 0.05
})
return material
};
接下来所有的线条都基于此方法进行创建。
效果图
挤压缓冲几何体
fetchHZSMapData().then(...)
,用同样的方法请求到需要制作挤压缓冲几何体的顶点信息,或者直接用刚才过滤出来的顶点信息制作缓冲几何体
const createShape = (positions: number[]) => {
const shape = new THREE.Shape();
// 绘制形状轮廓
shape.moveTo(positions[0], positions[2]);
for (let i = 3; i < positions.length; i += 3) {
shape.lineTo(positions[i], positions[i + 2]);
}
shape.lineTo(positions[0], positions[2]); // 闭合路径
// 设置挤压参数
const extrudeSettings = {
steps: 1, // 挤压的分段数
depth: 0.04, // 挤压的深度
bevelEnabled: false, // 是否启用斜角
};
// 创建挤压几何体和网格
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const shapeMesh = new THREE.Mesh(geometry, [extrudeMaterial(0x1c212c), extrudeMaterial(0x12141c)]);
shapeMesh.rotation.x = Math.PI / 2; // 旋转使其与路径方向一致
shapeMesh.userData.isLight = false
return shapeMesh
}
定义一个createShape
的函数,其主要目的是基于传入的顶点坐标数组创建一个三维形状的网格对象。这个三维形状是通过先绘制二维形状轮廓,再对其进行挤压操作得到的,最后返回设置好相关属性(如旋转角度、用户自定义数据等)的三维网格对象,可用于三维图形渲染等相关场景。
positions
:从请求中获取的二维顶点信息并通过前文提到的getPositions
方法处理过的数组,数组中的元素按每三个一组的方式代表各个顶点在三维空间中的坐标(这里实际使用时主要关注每组中的第一个和第三个元素来绘制二维形状)。
首先创建一个THREE.Shape
对象,用于绘制二维形状。
通过shape.moveTo(positions[0], positions[2])
将绘制起点设置为传入坐标数组中第一个顶点的x
和z
坐标(这里似乎是在xz
平面上绘制二维形状)。
接着使用for
循环,从索引为3
开始,每次递增3
遍历坐标数组。在循环中,通过shape.lineTo(positions[i], positions[i + 2])
依次连接各个顶点,绘制出二维形状的轮廓。
最后通过shape.lineTo(positions[0], positions[2])
将路径闭合,形成完整的二维形状。
创建一个名为extrudeSettings
的对象,用于设置挤压操作的相关参数。
其中steps
属性设置为1
,表示挤压过程的分段数为1
;depth
属性设置为0.04
,确定了挤压的深度;bevelEnabled
属性设置为false
,表示不启用斜角效果。
利用之前创建的二维形状对象shape
和设置好的挤压参数extrudeSettings
,通过调用new THREE.ExtrudeGeometry(shape, extrudeSettings)
创建一个挤压几何体。
然后创建一个THREE.Mesh
对象,即三维网格对象。将创建好的挤压几何体作为其几何属性,同时传入两个材质对象,作为其材质属性。第一个材质为表面材质,第二个为侧面材质。
- 挤压缓冲几何体材质
export const extrudeMaterial = (color: number) => new THREE.MeshLambertMaterial({
color,
});
效果图
在开发过程中由于3d场景的复杂性,以及灯光和环境对模型颜色的影响,如果直接应用UI提供设计稿的颜色,并不能完全的复刻UI稿,所以就需要一个GUI参数来实时调整材质的颜色,手动将颜色调为和UI稿一致
所以我们现在创建一个GUI并添加到dom中。
图形用户界面(GUI
创建一个图形用户界面(GUI)来配置与挤压缓冲结合体相关的一些参数,并提供了对几何体材质颜色的可调节功能。通过这个 GUI,用户可以直观地修改诸如形状的顶面颜色、侧面颜色等参数,以便实时看到挤压体相关元素外观的变
const gui = new GUI({
container: document.getElementById('gui') as HTMLElement, width: 300, title: '地图配置' });
export const guiParams = {
shapeColor: 0xffffff,
shapeColor2: 0xffffff,
...
};
const shapeColor = gui.addColor(guiParams, 'shapeColor')
shapeColor.name('顶面颜色')
export {
shapeColor }
const shapeColor2 = gui.addColor(guiParams, 'shapeColor2')
shapeColor2.name('侧面颜色')
export {
shapeColor2 }
创建一个名为guiParams
的对象,并将其导出,以便在其他模块中也能使用这个对象来获取或修改相关参数。
添加颜色调节控件(针对顶面颜色)
使用gui.addColor
方法(这是 GUI 库提供的用于添加颜色调节控件的方法),从guiParams
对象中获取shapeColor
属性的值,并在 GUI 上创建一个颜色调节控件。这个控件允许用户修改guiParams
对象中shapeColor
属性的值。
该方法返回一个对象(赋值给shapeColor
变量),这个对象可以进一步进行一些设置操作,比如设置控件的名称等
下面是调整挤压缓冲几何体材质颜色的方法
// 设置GUI控制挤压体的颜色
guiParams.shapeColor = shape.material[0].color.getHex();
shapeColor.updateDisplay()
shapeColor.onChange(function (val) {
shape.material[0].color.setHex(val)
});
guiParams.shapeColor2 = shape.material[1].color.getHex();
shapeColor2.updateDisplay()
shapeColor2.onChange(function (val) {
shape.material[1].color.setHex(val)
});
设置 GUI 参数的初始值
guiParams.shapeColor = shape.material[0].color.getHex();
shapeColor.updateDisplay();
color.getHex()
是调用该材质对象的 color
属性的 getHex()
方法,其作用是获取当前材质颜色的十六进制表示值。然后将这个值赋给 guiParams
对象的 shapeColor
属性,guiParams
是用于集中管理 GUI 相关参数的对象,这样就将挤压体第一种材质的当前颜色设置为了 GUI 中对应颜色控制参数的初始值。
定义颜色修改的回调函数
shapeColor.onChange(function (val) {
shape.material[0].color.setHex(val)
});
shapeColor.onChange()
是为 shapeColor
对象在 GUI 中的颜色控制组件注册一个 onChange
回调函数。当用户在 GUI 界面上通过与该颜色控制组件相关的操作,修改颜色值时,这个回调函数就会被触发。
调节颜色效果图
通过前面绘制线条的方法,如法炮制绘制出其他的装饰线条并在GUI中修改到合适的颜色
效果图
告警标记
下面代码以最外层带动画的线条举例,从创建到动画的过程。从前文效果图可以看出 这里的线组成了一个六边形,并在运动过程中通过改变六边形的角,而向外扩散。
warn && (() => {
const length = 0.018; // 假设每条线的长度为0.018
const distance = length * 2; // 移动的距离
const maxDistance = distance * 2.5; // 移动的距离
const {
group: tagLineGroup, tween: tagLineTween } = tagLine(color, distance, maxDistance, length, 4, warn)
group.add(tagLineGroup)
tweenGroup.add(tagLineTween)
})();
这里使用了逻辑与(&&
)运算符的短路特性。如果 warn
变量的值为 false
,那么整个表达式就会立即返回 false
,后面的匿名函数就不会被执行;只有当 warn
为 true
时,才会执行后面的匿名函数,进入到具体的操作流程中。
首先定义了一个常量 length
,并将其值设置为 0.018
然后根据 length
计算出 distance
,它是线初始距离,通过 length
乘以 2
得到。最终组成一个完整而不重叠的六边形
最后计算出 maxDistance
,移动的距离,通过 distance
乘以 2.5
得到。移动后的效果即展开六边形的角
tagLine 方法
const tagLine = (color: number, distance: number, maxDistance: number, length: number, width: number, warn = false) => {
const group = new THREE.Group(