文章目录
- 前言
- 飞线实现
- 1 初始化地图并加载three图层
- 2 绘制飞线几何体
- 将几何体正确定位在mapbox上
- 正确操作BufferGeometry几何体
- 3 tween实现动画
- 全部代码
- 总结
- 待改进之处
- 参考
前言
mapbox-gl是一个基于webgl开发的三维地图渲染引擎,但是很多三维特效只用mapbox并不容易实现,比如带高度的飞线,但是使用threejs就有多种实现的方式,本文结合之前对threebox的研究,使用一种使用线图层动画的方式实现带高度的飞线。
飞线实现
1 初始化地图并加载three图层
mapbox中操作threejs建议使用threebox,相比直接操作threejs会简便很多,通过添加一个自定义图层实现。
mapRef.current = new mapboxgl.Map({
zoom: 12,
center: [104.807073, 29.35702],
pitch: 60,
style: 'mapbox://styles/mapbox/dark-v11',
container: mapContainerRef.current,
antialias: true,
});
mapRef.current.on('load', () => {
mapRef.current.addLayer(
{
id: 'custom_layer',
type: 'custom',
onAdd: function (map, mbxContext) {
this.map = map;
tbRef.current = new Threebox(
map,
mbxContext,
{ defaultLights: true }
);
let obj3D = draw2();
tbRef.current.add(obj3D);
},
render: function (gl, matrix) {
if (this.map) {
this.map.triggerRepaint();
}
tbRef.current.update();
TWEEN.update();
}
}
)
})
2 绘制飞线几何体
实现动画飞线的效果基本思路很直接:建立路径后,在线上截取一段作为飞痕,设置好颜色后使其沿着路线不断运动即可。在这一步中,关键的有两点:
将几何体正确定位在mapbox上
mapbox是3857坐标系,threejs是右手空间坐标系,在使用three的方法前要将坐标进行转换:
let startPoint = [104.807073, 29.35702]
let endPoint = [105.807073, 30.35702]
// 坐标转换
const xyz_start = tbRef.current.utils.lnglatsToWorld([[...startPoint, 0]])
const xyz_end = tbRef.current.utils.lnglatsToWorld([[...endPoint, 0]])
// 创建3d飞线路径
const pointInLine = [
new THREE.Vector3(xyz_start[0].x, xyz_start[0].y, 0),
new THREE.Vector3((xyz_start[0].x + xyz_end[0].x) / 2, (xyz_start[0].y + xyz_end[0].y) / 2, curveH),
new THREE.Vector3(xyz_end[0].x, xyz_end[0].y, 0)
];
正确操作BufferGeometry几何体
操作几何体参考之前有讲,一些核心方法在本例中常用。
// 为几何体attributes存储顶点信息
flyLineGeom.setFromPoints(pointsList);
// 几何体根据attributes中的position绘制
flyLineGeom.attributes.position.needsUpdate = true;
// 几何体attributes存储颜色信息
flyLineGeom.attributes.color = new THREE.BufferAttribute(new Float32Array(colorArr), 3);
// 几何体根据attributes中的color信息上色
flyLineGeom.attributes.color.needsUpdate = true;
// 几何体渲染时颜色采用geometry.attributes.color中对应的颜色
const material2 = new THREE.LineBasicMaterial({
vertexColors: THREE.VertexColors, //使用顶点本身颜色
});
3 tween实现动画
tween可以方便实现动画,每帧跟新飞痕的位置
let tween = new TWEEN.Tween({ index: 1 })
.to({ index: 50 }, 2000)
.onUpdate(function (t) {
let id = Math.ceil(t.index);
let pointsList = points.slice(id, id + 10); //从曲线上获取一段
flyLineGeom && flyLineGeom.setFromPoints(pointsList);
flyLineGeom.attributes.position.needsUpdate = true;
}) .repeat(Infinity)
tween.start();
全部代码
import React, { useRef, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
import { Threebox, THREE } from 'threebox-plugin';
import TWEEN from '@tweenjs/tween.js'
function App() {
const mapContainerRef = useRef();
const tbRef = useRef();
const mapRef = useRef();
// 初始化基础图层
useEffect(() => {
mapboxgl.accessToken = 'pk.eyJ1IjoiamFja2Vyb28iLCJhIjoiY2w5aThzdWJ5MTBjYjQybzg1cHNhYXFqeCJ9.HmUVfVzRx1EOl4uzY-HFuQ'
mapRef.current = new mapboxgl.Map({
zoom: 12,
center: [104.807073, 29.35702],
pitch: 60,
style: 'mapbox://styles/mapbox/dark-v11',
container: mapContainerRef.current,
antialias: true,
});
mapRef.current.on('load', () => {
mapRef.current.addLayer(
{
id: 'custom_layer',
type: 'custom',
onAdd: function (map, mbxContext) {
this.map = map;
tbRef.current = new Threebox(
map,
mbxContext,
{ defaultLights: true }
);
let obj3D = draw2();
console.log(obj3D)
tbRef.current.add(obj3D);
},
render: function (gl, matrix) {
if (this.map) {
this.map.triggerRepaint();
}
tbRef.current.update();
TWEEN.update();
}
}
)
})
}, []);
function draw2() {
const curveH = 800; // 飞线最大高
const lineGroup = new THREE.Group();
lineGroup.name = 'lineGroup';
let startPoint = [104.807073, 29.35702]
let endPoint = [105.807073, 30.35702]
const xyz_start = tbRef.current.utils.lnglatsToWorld([[...startPoint, 0]])
const xyz_end = tbRef.current.utils.lnglatsToWorld([[...endPoint, 0]])
const pointInLine = [
new THREE.Vector3(xyz_start[0].x, xyz_start[0].y, 0),
new THREE.Vector3((xyz_start[0].x + xyz_end[0].x) / 2, (xyz_start[0].y + xyz_end[0].y) / 2, curveH),
new THREE.Vector3(xyz_end[0].x, xyz_end[0].y, 0)
];
// 创建轨迹线
const curve = new THREE.CatmullRomCurve3(pointInLine);
const points = curve.getSpacedPoints(50);
const lineGeom = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color:0x006666
});
const curveObject = new THREE.Line(lineGeom, material);
// 创建移动的线
const index = 20; //取点索引位置
const num = 10; //从曲线上获取点数量
const points2 = points.slice(index, index + num); //从曲线上获取一段
const flyLineGeom = new THREE.BufferGeometry();
flyLineGeom.setFromPoints(points2);
// 操作颜色
const colorArr = [];
for (let i = 0; i < points2.length; i++) {
const color1 = new THREE.Color(0x006666); // 线颜色
const color2 = new THREE.Color(0xffff00); //飞痕颜色
// 飞痕渐变色
let color = color1.lerp(color2, i / 5)
colorArr.push(color.r, color.g, color.b);
}
// 设置几何体顶点颜色数据
flyLineGeom.attributes.color = new THREE.BufferAttribute(new Float32Array(colorArr), 3);
flyLineGeom.attributes.position.needsUpdate = true;
const material2 = new THREE.LineBasicMaterial({
vertexColors: THREE.VertexColors, //使用顶点本身颜色
});
const curveFlyObject = new THREE.Line(flyLineGeom, material2);
lineGroup.add(curveObject,curveFlyObject)
// 创建动画
let tween = new TWEEN.Tween({ index: 1 })
.to({ index: 50 }, 2000)
.onUpdate(function (t) {
let id = Math.ceil(t.index);
let pointsList = points.slice(id, id + 10); //从曲线上获取一段
flyLineGeom && flyLineGeom.setFromPoints(pointsList);
flyLineGeom.attributes.position.needsUpdate = true;
})
.repeat(Infinity)
tween.start();
return lineGroup
}
return (
<div style={{ display: 'flex' }}>
<div
id="map-container"
ref={mapContainerRef}
style={{ height: '100vh', width: '100vw' }}
/>
<div style={{ position: 'fixed', top: '0', right: '0' }}>
</div>
</div>
);
}
export default App;
总结
飞线实现
- 1 初始化地图并加载three图层
- 2 绘制飞线几何体
- 3 tween实现动画
待改进之处
本文采用简单的思路实现了在mapbox地图中加入three飞线,但是还有改进方案,比如:
- 通过更改geometry.attributes.color实现动画效果,不需要额外新建几何体
- 通过着色器实现更加生动的效果
参考
- mapboxgl + three.js 开发实践