VUE中Three.js与Cesium.js的合成---【入门级别】

1.背景

  前期在学习调用模型的过程中我接触了three.js,知道了它非常便于调用不同类型的3D模型,比如ply,fbx,pcd。但是调出来的模型过于单调,再加上调用的模型本就是处于现实生活场景中的,经过老师的建议,决定给模型加上合适的背景,这时从老师那里了解到cesium可以调出整个地球模型,于是就开始了对cesium的探索之路。

  经过查阅,发现cesium有着丰富的功能,他本身不仅内置了高度精确的地球模型,还支持动画、交互等,但是和three不一样的是它只支持gltf或glb类型的模型文件。当时的第一个想法当然是去寻找cesium里面是否自带某种文件类型的转换方法,毕竟它如此强大,找了一圈却没有看到相关功能。后来我的同伴将需要调用的ply文件通过插件手动转成了glb,在cesium里面成功调用。

  到这里都还没有将两者结合的必要,到了后期我们收到了模型上以及模型周围的三维坐标数组,要求标出对应位置、描绘出点位的轨迹。这个功能我们前期在three里面已经成功解决,但是cesium里面需要提供的却是经纬度和高。对于给出的坐标有着其独立参考的坐标原点,我很难想象这个坐标和经纬度之间如何转换(如果有相关方法欢迎大佬告知),于是就想到了在cesium已调出的地球面貌的背景下再调出three的模型,将three用在cesium的图层上。

2.思路及过程

  在查阅资料时,发现了两种办法有探索成功的可能:(1)将three和cesium进行较深层次的融合,包含了相机的一致,通过复制 Cesium 的球形坐标系和匹配两个场景中的数字地球,将两个独立的渲染引擎层集成到一个主场景中。方法(1)对于我这种入门都未曾达到的人来说不算简单,所以我果断选择了第二种:(2)考虑到只需演示静态模型,对加载的速度和动画效果就没有要求,我选择了前辈在他的文章中提过的一个方法:使用GLTFExporter将three里面的场景进行打包,再发给cesium进行调用。

     逻辑上可以实现,那就直接开始执行。考虑到后期方便多次调用二者,我直接创建了第三个组件去调用二者,利用子传父,父传子的数据流动方式将three里面打包的东西通过第三方组件再传给cesium。理想是丰满的,现实是骨感的。不知道为什么在调用的时候,总是会先执行cesium,再去执行three,这会导致拿到模型数据之前地球就已经渲染,尝试多次解决无果,只好将二者的代码写在一个组件再去调用。

  放在一个组件之后才发现,是函数执行的顺序,我们要调用一个有返回结果的函数,需要进行回调,而不能直接调用赋值,所以手动写了一个回调函数,成功将three打包的url传给了cesium去渲染。新的问题又随之出现,在cesium里面渲染出来的模型只有加在three场景里面的圆点和坐标轴,却没有出现模型,同伴发现模型是放在mesh网格里,再加载在场景scene里面,所以他对mesh进行了单独的打包,居然可行!!写了两个回调函数分别对mesh和scene进行了打包,在回调函数里面同时调用同一个cesium调模型的方法,大功告成。现附上代码以供参考

  打点和连线其实可以融合成一个方法,但想到后期连线的逻辑可能不会一致,就单独拿出来封装了一个。这里五个方法之间的层次调用关系值得再完善和改进,欢迎大佬不吝赐教。

methods: {
        // 构建three场景,打包scene
        three_init(callback_mesh, callback_scene) {
            // 创建场景、相机和渲染
            const scene = new THREE.Scene();
            // const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            // const renderer = new THREE.WebGLRenderer({
            //     antialias: true,
            //     alpha: true
            // });
            // renderer.setSize(window.innerWidth, window.innerHeight);
            // const container = document.getElementById('id');
            // container.appendChild(renderer.domElement);
            // const controls = new OrbitControls(camera, renderer.domElement);
            // controls.addEventListener('change', () => {
            //     renderer.render(scene, camera) // 监听鼠标,键盘事件
            // })
            // 创建灯光
            const ambientLight = new THREE.AmbientLight(0x404040);
            scene.add(ambientLight);

            const directionalLight = new THREE.DirectionalLight(0xffffff);
            directionalLight.position.set(1, 1, 1).normalize();
            scene.add(directionalLight);

            // function animate() {
            //     requestAnimationFrame(animate);
            //     renderer.render(scene, camera);
            // }

            // 创建加载器
            const src = '你的模型路径';
            const loader = new PLYLoader();
            loader.load(src, geometry => {
                const material = new THREE.MeshStandardMaterial({color: 0x00ff00, roughness: 0.8, metalness: 0.2});
                const mesh = new THREE.Mesh(geometry, material);
                //打包mesh
                exporter.parse(mesh, (result) => {
                    if (result instanceof ArrayBuffer) {
                        const blob = new Blob([result], {type: 'application/octet-stream'});
                        const url = URL.createObjectURL(blob);
                        callback_mesh(url)
                    } else {
                        const blob = new Blob([JSON.stringify(result)], {type: 'application/json'});
                        const url = URL.createObjectURL(blob);
                        console.log('three_inti里面的url:' + url)
                        callback_mesh(url); // 调用回调函数并传递 URL
                    }
                }, {binary: true});
                // scene.add(mesh);
            })
            // 添加坐标轴
            const axesHelper = new THREE.AxesHelper(300); // 300是坐标轴的长度
            scene.add(axesHelper);
            // camera.position.z = 5;
            // animate();
            const datalist=[]//你的三维坐标数组
            this.showpoints(datalist, 0xff0000, scene)
            this.show_line(datalist, 0xff0000, scene)

            // 打包scene
            const exporter = new GLTFExporter();
            exporter.parse(scene, (result) => {
                console.log(result)
                if (result instanceof ArrayBuffer) {
                    const blob = new Blob([result], {type: 'application/octet-stream'});
                    const url = URL.createObjectURL(blob);
                    callback_scene(url)
                } else {
                    const blob = new Blob([JSON.stringify(result)], {type: 'application/json'});
                    const url = URL.createObjectURL(blob);
                    console.log('three_inti里面的url:' + url)
                    callback_scene(url); // 调用回调函数并传递 URL
                }
            }, {binary: true});
        },
        // 打点
        showpoints(pointslist, color, scene) {
            if (pointslist == null) {
                return
            }
            // 打点
            pointslist.forEach(point => {
                const sphereGeometry = new THREE.SphereGeometry(1, 32, 32); // 半径为5,细分度32x32
                const sphereMaterial = new THREE.MeshBasicMaterial({color: color}); // 红色
                const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
                sphere.position.set(...point); // 设置球体的位置
                scene.add(sphere); // 将             球体添加到场景中
            });
        },
        // 连线:这里是我自己连线的逻辑,可以自行修改
        show_line(pointslist, color, scene) {
            if (pointslist.length % 2 == 0) {
                for (let i = 0; i < pointslist.length - 1; i += 2) {
                    const startPoint = pointslist[i];
                    const endPoint = pointslist[i + 1];
                    // 创建线段的几何体
                    const geometry = new THREE.BufferGeometry().setFromPoints([
                        new THREE.Vector3(...startPoint),
                        new THREE.Vector3(...endPoint)
                    ]);

                    // 创建线段的材料(这里使用LineBasicMaterial)
                    const material = new THREE.LineBasicMaterial({color: color}); 

                    // 创建线段并添加到场景中
                    const line = new THREE.Line(geometry, material);
                    scene.add(line);
                }
            }
        },
        // 在cesium里面调用模型
        show_model(glbdata, viewer, Cesium) {
            // 添加模型
            const position = Cesium.Cartesian3.fromDegrees(
                106.613922,
                29.53832,
                0
            );
            //这里分别表示xyz的转向,我们根据实际模型调整了角度,默认为0
            const heading = Cesium.Math.PI/2;
            const pitch = Cesium.Math.PI/2;
            const roll = Cesium.Math.PI/2;
            const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
            const orientation = Cesium.Transforms.headingPitchRollQuaternion(
                position,
                hpr
            );

            const entity = viewer.entities.add({
                position: position,
                orientation: orientation,
                model: {
                    uri: glbdata,
                    minimumPixelSize: 128,
                    maximumScale: 20000,
                    scale: 1, // 缩放比例
                    incrementallyLoadTextures: true, // 加载模型后的纹理是否可以继续流入
                    runAnimations: true, // 指定是否应运行 glTF 动画
                    clampAnimations: true, // 指定 glTF 动画是否应在不带关键帧的持续时间内保持最后一个姿势
                    shadows: Cesium.ShadowMode.ENABLED, // 指定模型是否 投射或接收来自光源的阴影
                    heightReference: Cesium.HeightReference.NONE, // 表示相对于地形的位置。NONE表示绝对位置
                    // heightReference: Cesium.HeightReference.CLAMP_TO_GROUND // 相对位置
                }
            });
            viewer.trackedEntity = entity;
        },
        // 创建cesium地球模型,调出打包的scene
        cesium_init() {
            (Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0OWEzNzU5YS1jNWVlLTQ1ODMtOGNiOS02ZTI1NWQ1MmQxNmYiLCJpZCI6MjI5NTcwLCJpYXQiOjE3MjE5MDcwODd9.wBt9x6gZU6Rdhyo7OFQmkclqLnpyi79zA8SKX5L0NcA');
            this.viewer = new Cesium.Viewer('my-map', {
                homeButton: false,
                sceneModePicker: false,
                baseLayerPicker: false, // 影像切换
                animation: true, // 是否显示动画控件
                infoBox: false, // 是否显示点击要素之后显示的信息
                selectionIndicator: false, // 要素选中框
                geocoder: true, // 是否显示地名查找控件
                timeline: true, // 是否显示时间线控件
                fullscreenButton: false,
                shouldAnimate: false,
                navigationHelpButton: false, // 是否显示帮助信息控件
            });
            // 隐藏版权信息
            this.viewer.cesiumWidget.creditContainer.setAttribute('style', 'display:none')

            // 优化第一步
            // 这是让你的画面以一个怎样的形式出现,相当于出场动画
            this.viewer.camera.flyTo({
                // fromDegrees()方法,将经纬度和高程转换为世界坐标,这里定位到中国
                destination: Cesium.Cartesian3.fromDegrees(106.613922, 29.53832, 1000),
                orientation: {
                    roll: 0.0
                }
            });

            this.three_init((mesh_url) => {
                console.log('从 three_init 返回的 URL:', mesh_url);
                this.show_model(mesh_url, this.viewer, Cesium)
            },
                (scene_url) => {
                    console.log('从 three_init 返回的 URL:', scene_url);
                    this.show_model(scene_url, this.viewer, Cesium)
                });
            // 优化第二步
            // 显示地名:这里根据需求可以注释,如果运行会报请求次数过多的错误,但不影响模型展示
            this.viewer.imageryLayers.addImageryProvider(
                new Cesium.WebMapTileServiceImageryProvider({
                    url:
                        "http://{s}.tianditu.gov.cn/cva_c/wmts?service=wmts&request=GetTile&version=1.0.0" +
                        "&LAYER=cva&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}" +
                        "&style=default&format=tiles&tk=0a30a060a83ae06ba7a9dd5f70a3c203",
                    layer: "tdtCva",
                    style: "default",
                    format: "tiles",
                    tileMatrixSetID: "c",
                    subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
                    tilingScheme: new Cesium.GeographicTilingScheme(),
                    tileMatrixLabels: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",],
                    maximumLevel: 18,
                    show: false,
                })
            );
        }
    },
3.结语

  cesium只支持gltf和glb的原因之一或许是glb会自动将模型立在地球表面或者自动将模型底边与地球表面重合,因为同伴将ply模型转成glb模型调出来的时候,就已经立于地球表面,但是我们通过three在cesium里面调出来的模型却是“歪七八扭”的,没有办法,只好手动调制。  

  还有一点值得注意的是,在three里面和cesium里面都有对相机方位的控制,我们在两者的结合过程中,应该舍弃一方,不然可能会出问题,这里我将代码稍微好理解的three里的相机和灯光给取消了,只保留scene和mesh(只保留了与打点连线和加载模型)有关的代码。

  诚然,在两者结合的过程中,我对这两个工具背后所支撑的庞大的功能了解还不够透彻。比如cesium的动画效果,影像叠加,交互功能等等,它与three的融合我也只领略到了皮毛,看了不少优秀博客发布的两者的融合也已经做的足够深入。信息战嘛,了解与未知之间的差距是一条巨大的鸿沟,学无止境,欢迎各位前辈不吝赐教。

  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值