js相对视口的高度_mapboxgl + three.js 开发实践

本文介绍了如何在mapboxgl中使用three.js和threebox插件扩展3D功能,特别是创建带岛洞的空间体和OD飞线效果。通过Delaunay三角剖分算法进行多边形处理,并探讨了three.js中对象的坐标转换和透明度设置问题。
摘要由CSDN通过智能技术生成

一、前言

mapbox样式标准中,空间要素的高度属性仅仅在fill-extrusion layer中有体现。 如果想在场景中添加具有高程属性的,标注、点、线或面图层是做不到的。除非我们自定义图层来操作webgl。我在风力气象可视化实践(知乎、掘金)中也有实践过,自行编写着色代码,操作显存,无疑是一件很痛苦的事情。我们使用three.js来扩展mapboxgl的能力。threebox是一个很好的插件,帮助我们在mapboxgl和three.js之间建立联接,可以方便的将空间对象添加到场景中。
threebox已经提供了多个案例,我在测试学习实践过程中写了两个案例,也遇到了一些问题,记录总结一下。欢迎指正。

  • 绘制一个带导洞的空间体
  • 实现一个OD飞线效果

二、 绘制一个带岛洞的空间体

这里没有使用three.js创建shape对象,使用ExtrudeGeometry来将对象拉升高度实现。是通过创建顶点,三角面片的方式来创建一个geometry对象。

问题:我的理解,threejs中应该是没有度量单位,我如果在mapboxgl中使用threejs创建ExtrudeGeometry,指定拉升高度改如何实现。直接设置depth,该设置多少?

1、three.js长方体

在threejs中直接绘制一个长方体比较比较简单,定义长方体的六个顶点,和十二个三角面片。

 let cubeGeometry = new THREE.Geometry();
        // 创建立方体的顶点
        let vertices = [
            new THREE.Vector3(10, 10, 10), //v0
            new THREE.Vector3(-10, 10, 10), //v1
            new THREE.Vector3(-10, -10, 10), //v2
            new THREE.Vector3(10, -10, 10), //v3
            new THREE.Vector3(10, -10, -10), //v4
            new THREE.Vector3(10, 10, -10), //v5
            new THREE.Vector3(-10, 10, -10), //v6
            new THREE.Vector3(-10, -10, -10) //v7
        ];
        cubeGeometry.vertices = vertices;
        //创建立方的面
        let faces = [
            new THREE.Face3(0, 1, 2),
            new THREE.Face3(0, 2, 3),
            new THREE.Face3(0, 3, 4),
            new THREE.Face3(0, 4, 5),
            new THREE.Face3(1, 6, 7),
            new THREE.Face3(1, 7, 2),
            new THREE.Face3(6, 5, 4),
            new THREE.Face3(6, 4, 7),
            new THREE.Face3(5, 6, 1),
            new THREE.Face3(5, 1, 0),
            new THREE.Face3(3, 2, 7),
            new THREE.Face3(3, 7, 4)
        ];
        cubeGeometry.faces = faces;
        //生成法向量
        cubeGeometry.computeFaceNormals();
        let cubeMaterial = new THREE.MeshLambertMaterial({ color: 0x00ffff });
        cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
        scene.add(cube);

a3f6251e9dd1052ea918e7d9cba666e9.png
three.js长方体

2、threebox长方体

使用threebox结合mapboxgl绘制一个长方体。指定边长单位:米,将长方体放在经纬度(120.6827139, 31.2970519)高度50米处。

       map.addLayer({
           id: 'custom_layer',
           type: 'custom',
           onAdd: function (map, mbxContext) {
               tb = new Threebox(
                   map,
                   mbxContext,
                   { defaultLights: true }
               );
               let geometry = new THREE.BoxGeometry(20, 20, 20);
               let redMaterial = new THREE.MeshPhongMaterial({
                   color: 0x009900,
                   // side: THREE.DoubleSide
               });
               let cube = new THREE.Mesh(geometry, redMaterial);
               // 单位设置为米
               cube = tb.Object3D({ obj: cube, units: 'meters', })
                   .setCoords([120.6827139, 31.2970519, 50]) // 经纬度,高度为米

               tb.add(cube)
           },
           render: function (gl, matrix) {
               tb.update();
           }
       });

1b43984d25d461231d5794003c9172c3.png
threebox 正方体

3、带岛洞的空间体

使用threebox在mapboxgl中绘制一个带有导洞的多边形,首先需要对多边形进行三角剖分。就像前面绘制一个长方体一样,需要指定长方体顶点和三角面片,即组成正方体需要哪些顶点组成的三角面片。这是一个稍有复杂的算法。cdt2d是一个使用JavaScript编写的Delaunay三角剖分的库。它可以帮我们实现对多边形进行三角剖分。这里注意的是,它是一个二维的三角剖分算法库。

1)Delaunay三角剖分

let cdt2d = require('cdt2d')
// 指定顶点坐标
let points = [
  [-2,-2],
  [-2, 2],
  [ 2, 2],
  [ 2,-2],
  [ 1, 0],
  [ 0, 1],
  [-1, 0],
  [ 0,-1]
]
// 指定边,注意顶点顺序,每个面都是一个闭合的点串
// 前四个边组成一个面--外圈
// 后四个边组成一个面--内圈
let edges = [
 //Outer loop
 [0, 1], // 第一个顶点和第二个顶点组成的边
 [1, 2],
 [2, 3],
 [3, 0],
 //Inner loop
 [4, 5],
 [5, 6],
 [6, 7],
 [7, 4]
]
// 输出三角剖分结果
[[0,3,7],[0,6,1],[0,7,6],[1,5,2],[1,6,5],[2,4,3],[2,5,4],[3,4,7]]
console.log(cdt2d(points, edges, {exterior: false}))

2)绘制带有岛洞的多边形

下面直接贴出结果和代码,直接看代码可能好理解一些。我们的目标是,构建进行三角剖分的顶点和边。然后通过Delaunay三角剖分算法,得到构成图形的三角面片。最后通过顶点和面片结果创建three对象,添加到场景中。构建用于三角剖分的边有点绕,很容易出错,不理解的可以看看上面三角剖分的案例。

threebox中绘制坐标需要将其转换成web投影坐标。normalizeVertices函数做了两件事情,首先,创建一个外包椭球将所有顶点包围起来,并计算出外包椭球的中心坐标。然后、用原始顶点坐标减去外包球的中心(向量减法)就得到原始顶点相对于外包球中心点的坐标。这么做的目的是通过设置mesh的中心点坐标是外包球的中心,就可以将我们绘制的图形放置指定位置,这是threebox在地图中绘制对象的方式。

  normalizeVertices(vertices) {
        var geometry = new THREE.Geometry();
        for (v3 of vertices) {
            geometry.vertices.push(v3)
        }
        geometry.computeBoundingSphere();
        var center = geometry.boundingSphere.center;
        var radius = geometry.boundingSphere.radius;
        var scaled = vertices.map(function(v3){
            var normalized = v3.sub(center);
            return normalized;
        });
        return {vertices: scaled, position: center}
    }

3bdec687db42aeb3d5ff771acbf6424c.png
threebox多边形
       map.on('style.load', function () {
       // 标准 geojson 格式数据 
            jQuery.get('../../data/dd2.json').then(data => {
                map.addLayer({
                    id: 'custom_layer',
                    type: 'custom',
                    onAdd: function (map, mbxContext) {
                        tb = new Threebox(
                            map,
                            mbxContext,
                            { defaultLights: true }
                        );
                        data.features.forEach(feature => {
                            let geoCor = feature.geometry.coordinates;
                            // 存储顶点 [lang,lat,0]
                            let vertexs = [];
                            // 存储顶点向量 THREE.Vector3(x, y, z) ;
                            const vertexsVectors = [];
                            // 存储图形边,用来进行三角剖分
                            const edges = [];
                            // 存储图形顶点,用来进行三角剖分
                            const points = [];
                            // 含有导洞的多边形顶点,序号是递增的,不管它有几个多边行
                            let tindex = 0;
                            // 构建用于三角剖分的顶点和边、查看上面三角剖分,顶点和边的规则
                            geoCor.forEach((coors, i) => {
                                let firstCoordIndex = tindex;
                                let max = coors.length - 1;
                                coors.forEach((coor, i) => {
                                    if (i == max) return;
                                    vertexs.push([...coor, 0]);
                                    if (i < max - 1) {
                                        edges.push([tindex, (tindex + 1)]);
                                    } else {
                                        edges.push([tindex, firstCoordIndex]);
                                    }
                                    tindex++;
                                });
                            })
                            // 将顶点经纬度坐标,转换成web墨卡托投影坐标
                            const worldCoors = tb.utils.lnglatsToWorld(vertexs);
                            // 将世界坐标进行标准化处理
                            const normalized = tb.utils.normalizeVertices(worldCoors);
                            const { vertices, position } = normalized;
                            for (let i = 0; i < vertices.length; i++) {
                                const { x, y, z } = vertices[i];
                                vertexsVectors.push(new THREE.Vector3(x, y, z));
                                points.push([x, y, z])
                            }
                            // 进行三角剖分
                            let triangles = cdt2d(points, edges, { exterior: false });
                            let faces = [];
                            // 构建三角面片
                            triangles.forEach(item => {
                                faces.push(new THREE.Face3(...item))
                            })
                            // 创建 含有导洞的集合对象,并赋值顶点和三角面片
                            const geom = new THREE.Geometry();
                            geom.vertices = vertexsVectors;
                            geom.faces = faces;
                            // 计算法向量
                            geom.computeFaceNormals();
                            
                            const mesh = new THREE.Mesh(
                                geom,
                                new THREE.MeshLambertMaterial({
                                    color: 0x00ffff,
                                    //  wireframe:true,
                                })
                            );
                            mesh.castShadow = true;
                            // 设置对象在地图中的位置
                            mesh.position.copy(position);
                            tb.add(mesh);
                        })
                    },
                    render: function (gl, matrix) {
                        tb.update();
                    }
                });
            })
        });

3)绘制带有岛洞的多边体

绘制体就稍微麻烦点,主要是麻烦在得到所有三角面片,如果指定拉升5米高度。思路一样,底面构建出来,顶点其实不用构建,把三角面片所有顶点索引加上每个面的顶点数量就可以,然后在构建侧面,这里不再熬述。贴上我那糟糕的代码。

       data.features.forEach(feature => {
            let base = 0
            let CG = 5;
            let geoCor = feature.geometry.coordinates;
            let arras = [];
            let arrasbase = [];
            let arrasTop = [];
            const resVertices = [];
            const edges = [];
            const points = [];
            let tindex = 0;
            geoCor.forEach((coors, i) => {
                let firstCoordIndex = tindex;
                let max = coors.length - 1;
                coors.forEach((coor, i) => {
                    if (i == max) return;
                    arrasbase.push([...coor, base]);
                    arrasTop.push([...coor, base + CG]);
                    if (i < max - 1) {
                        edges.push([tindex, (tindex + 1)]);
                    } else {
                        edges.push([tindex, firstCoordIndex]);
                    }
                    tindex++;
                });
            })
            arras = [...arrasbase, ...arrasTop];
            const worldCoors = tb.utils.lnglatsToWorld(arras);
            const normalized = tb.utils.normalizeVertices(worldCoors);
            const { vertices, position } = normalized;
            for (let i = 0; i < vertices.length; i++) {
                const { x, y, z } = vertices[i];
                resVertices.push(new THREE.Vector3(x, y, z));
                points.push([x, y, z])
            }
            let topEdges = [];
            for (let item of edges) {
                topEdges.push([item[0] + edges.length, item[1] + edges.length])
            }
            let resEdges = [...edges, ...topEdges]
            points.length = points.length / 2;
            let triangles = cdt2d(points, edges, { exterior: false });
            let faces = [];
            triangles.forEach(item => {
                faces.push(new THREE.Face3(...item))
            })
            triangles.forEach(item => {
                faces.push(new THREE.Face3(item[0] + edges.length, item[1] + edges.length, item[2] + edges.length))
            })
            for (let i = 0; i < edges.length; i++) {
                let [a, b] = edges[i];
                faces.push(new THREE.Face3(a, b, a + edges.length))
                faces.push(new THREE.Face3(a + edges.length, b, b + edges.length))
            }
            const geom = new THREE.Geometry();
            geom.vertices = resVertices;
            geom.faces = faces;
            geom.computeFaceNormals();
            const mesh = new THREE.Mesh(
                geom,
                new THREE.MeshLambertMaterial({
                    color: 0x00ffff,
                    wireframe: true,
                })
            );
            mesh.castShadow = true;
            mesh.position.copy(position);
            tb.add(mesh);
        })

affa721bb69a1953c68634eae432163d.png
threebox不规则多面体

4)绘制建筑标准层数据

使用上代码,稍作修改,换一套数据,使用建筑标准层数据,来看一下效果。

通过使用 three.js new THREE.EdgesGeometry(geom)来给mesh添加边框线,结果他透明了,不知道为什么?怎么让他不透明。如果是不透明,看不到背后的边线,就不会出现这么混乱的效果。

6acdd3b9b0b411d23ec3fa3d89088540.png

三、OD飞线

用公交乘客上下车OD数据为例,写一个简单OD飞线效果。动画使用到了tween.js。功能比较简单,直接看代码,也很好理解。原理就是,三点创建弧线,指定插值点数量,使用tween.js指定动画时长,改变弧线顶点数量。就可以了。


乘客上车的站点ID | 站点维度 | 站点经度 | 乘客下车的站点ID | 站点维度 | 站点经度 | 下车人数        
002113ad-cb67,     31.31003,  120.7365,  013563fc-a796,    31.32696,   120.58606,3
   mapboxmap.on('style.load', function () {
            jQuery.get('./data/od.data').then(data => {
                mapboxmap.addLayer({
                    id: 'custom_layer',
                    type: 'custom',
                    onAdd: function (map, mbxContext) {
                        this.map = map;
                        tb = new Threebox(
                            map,
                            mbxContext,
                            { defaultLights: true }
                        );
                        let lineGroup = draw(tb, data);
                        tb.add(lineGroup);
                    },
                    render: function (gl, matrix) {
                        if (this.map)
                            this.map.triggerRepaint();
                        tb.update();
                        TWEEN.update();
                    }
                });
            })
        });
        function draw(tb, dataTxt, stationId = "4722ab93-c31f-457f-880a-47c5abfe5ae6") {
            // let levelH = 10; //水平基准高度
            let curveH = 10;
            let lineGroup = new THREE.Group();
            lineGroup.name = 'lineGroup';
            dataTxt.split('n').map(function (s, i) {
                let splitArray = s.split(',');
                if (splitArray[0] !== stationId) {
                    return;
                }
                let ll_o = [parseFloat(splitArray[1]), parseFloat(splitArray[2])].reverse();
                let xy_o = tb.utils.lnglatsToWorld([[...ll_o, 0]]);
                let ll_d = [parseFloat(splitArray[4]), parseFloat(splitArray[5])].reverse();;
                let xy_d = tb.utils.lnglatsToWorld([[...ll_d, 0]])
                let count = parseFloat(splitArray[6]);
                let color;
                let opacity;
                if (count > 0 && count <= 10) {
                    color = 60;
                    opacity = 0.3;
                } else if (count > 10 && count <= 30) {
                    color = 50;
                    opacity = 0.6;
                } else if (count > 30 && count <= 50) {
                    color = 40;
                    opacity = 0.8;
                } else if (count > 50 && count <= 100) {
                    color = 30;
                    opacity = 0.8;
                } else if (count > 100 && count <= 150) {
                    color = 20;
                    opacity = 0.8;
                } else if (count > 150) {
                    color = 10;
                    opacity = 0.8;
                }
                let curve = new THREE.CatmullRomCurve3([
                    new THREE.Vector3(xy_o[0].x, xy_o[0].y, 0),
                    new THREE.Vector3((xy_o[0].x + xy_d[0].x) / 2, (xy_o[0].y + xy_d[0].y) / 2, curveH),
                    new THREE.Vector3(xy_d[0].x, xy_d[0].y, 0)
                ]);
                let geometry = new THREE.Geometry();
                let curveModelData = curve.getPoints(50);
                geometry.vertices = curveModelData //.slice(0, 1);
                let material = new THREE.LineBasicMaterial({
                    color: new THREE.Color("hsl(" + color + ", 100%, 50%)"),
                    opacity: opacity,
                    transparent: true,
                    linewidth: 3,
                    blending: THREE.AdditiveBlending
                });
                let curveObject = new THREE.Line(geometry, material);
                curveObject.geometry.verticesNeedUpdate = true;
                let meshUserData = new Object();
                meshUserData.curveModelData = curveModelData;
                curveObject.userData = meshUserData;
                lineGroup.add(curveObject);
                tween = new TWEEN.Tween({ endPointIndex: 1 })
                    .to({ endPointIndex: 50 }, 3000)
                    .onUpdate(function (iii) {
                        let endPointIndex = Math.ceil(iii.endPointIndex);
                        let curvePartialData = new THREE.CatmullRomCurve3(curveModelData.slice(0, endPointIndex));
                        curveObject.geometry.vertices = curvePartialData.getPoints(50);
                        curveObject.geometry.verticesNeedUpdate = true;
                    })
                    .repeat(Infinity)
                tween.start();
            });
            return lineGroup;
        }

e5fb0c9b-a04d-eb11-8da9-e4434bdf6706.png

四、总结

使用threebox在mapboxgl中,通过自定义图层,来实现原生mapboxgl不容易实现的功能,或回避直接在自定义图层中直接操作webgl。实践了两个简单案例,总结回顾,错误难免,欢迎交流学习。后面若结合真实业务场景,有成果再分享,一起交流学习,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值