前言

大家好,我是南木元元,热衷分享有趣实用的文章。Three.js和Cesium.js是两个功能强大的JavaScript库,用于创建和渲染3D图形和场景。它们在某些方面有相似之处,但也有一些重要的区别。

  • Three是一个通用的3D图形库,适用于创建各种类型的3D场景和动画,主要用于构建游戏、虚拟现实、建筑可视化、产品展示等应用。
  • Cesium是一个地理可视化库,专注于地球模型和地理数据的展示和交互,主要用于构建GIS应用、地球科学研究、航空航天模拟等。

两者的定位和应用领域不同,所以为了更好地发挥它们各自的优势,本文就来分享一下如何将three和cesium结合起来,并在vue中使用。

两者结合的原理

cesium与Three.js有一些共同点:

  • 都是基于JavaScript的库
  • 支持WebGL
  • 提供丰富的3D渲染功能
  • 支持交互和动画

cesium的基本渲染原理与Three.js也没有太大的区别,通过在两个场景中复制cesium的球面坐标系和匹配的数字地球,很容易将两个单独的渲染引擎层整合到一个主场景中。主要过程如下:

  • 初始化Cesium渲染器
  • 初始化Three.js渲染器
  • 初始化这两个库的3D对象
  • 循环渲染器

这里主要参考了威尔逊等的将Three.js与Cesium集成的文章

vue中集成cesium和threejs

vue中集成cesium

首先需要创建一个vue项目,本文使用的是vue-cli4,这部分网上有很多文章,这里不再详细展开。

  • 安装cesium依赖
npm install cesium
  • 配置cesium

Cesium是一个庞大而复杂的库,除了 JavaScript 模块之外,它还包括静态资源,例如CSS、图像和 json 文件,还包括
Web Worker 文件,用于在单独的线程中执行密集计算,需要额外配置来确保Cesium的Assets、Widgets和 Web
Worker文件能正确提供和加载。在vue.config.js进行如下配置:

const CopyWebpackPlugin = require('copy-webpack-plugin')
const webpack = require('webpack')
const path = require('path')

let cesiumSource = './node_modules/cesium/Source'
let cesiumWorkers = '../Build/Cesium/Workers'

module.exports = {
    publicPath: "./",
    outputDir: "dist",
    lintOnSave: false,
    devServer: {
        open: process.platform === "darwin",
        host: "0.0.0.0",
        port: 5000,
        https: false,
        hotOnly: false
    },
    configureWebpack: {
        output: {
            sourcePrefix: ' '
        },
        amd: {
            toUrlUndefined: true
        },
        resolve: {
            alias: {
                'vue$': 'vue/dist/vue.esm.js',
                '@': path.resolve('src'),
                'cesium': path.resolve(__dirname, cesiumSource)
            }
        },
        plugins: [
            new CopyWebpackPlugin([{ from: path.join(cesiumSource, cesiumWorkers), to: 'Workers' }]),
            new CopyWebpackPlugin([{ from: path.join(cesiumSource, 'Assets'), to: 'Assets' }]),
            new CopyWebpackPlugin([{ from: path.join(cesiumSource, 'Widgets'), to: 'Widgets' }]),
            new CopyWebpackPlugin([{ from: path.join(cesiumSource, 'ThirdParty/Workers'), to: 'ThirdParty/Workers' }]),
            new webpack.DefinePlugin({
                CESIUM_BASE_URL: JSON.stringify('./')
            })
        ],
        module: {
            unknownContextCritical: /^.\/.*$/,
            unknownContextCritical: false
        }
    }
};
  • 全局引入cesium:配置main.js全局引入cesium相关文件
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

//引入cesium相关文件
var Cesium = require('cesium/Cesium');
var widgets = require('cesium/Widgets/widgets.css');

Vue.prototype.Cesium = Cesium
Vue.prototype.widgets = widgets

new Vue({
    render: h => h(App)
}).$mount('#app')
  • 测试cesium的使用:执行npm start,看是否能在浏览器看到初始化的三维地球,如果可以,就代表cesium环境已经搭建好了。
<template>
  <div id="container" class="box">
    <div id="cesiumContainer"></div>
  </div>
</template>
 
<script>
export default {
  name: 'Home',
  mounted(){
    this.init()
  },
  methods: {
    init() {
      let Cesium = this.cesium
      //这里的token使用自己注册的
      Cesium.Ion.defaultAccessToken = 'xxxxx'
      let viewer = new Cesium.Viewer('cesiumContainer');
    }
  }
};
</script>
 
<style lang='scss' scoped>
html,
body,
#cesiumContainer {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}
.box {
  height: 100%;
} 
</style>

引入threejs

安装threejs

npm install threejs

注意:threejs和ceiusm结合会有版本问题,具体可以参考这篇文章

实现效果

如下图所示,在cesium上方创建了两个three的图形,即完成了两者的结合。
在这里插入图片描述

完整代码

这里附上vue+cesium+threejs三者结合的代码:

<template>
    <div>
        <div id="cesiumContainer"></div>
        <div id="ThreeContainer"></div>
    </div>
</template>

<script>
import * as THREE from 'three'
export default {
    data () {
        return {
            minWGS84: [115.23, 39.55],
            maxWGS84: [116.23, 41.55],
            objects3D: [],
            three: {
                renderer: null,
                camera: null,
                scene: null
            },
            cesium: {
                viewer: null
            },
            ce: null,
        }
    },
    methods: {
        initCesium() {
            let Cesium = this.Cesium;
            // let cesiumContainer = document.getElementById("cesiumContainer");
            Cesium.Ion.defaultAccessToken = 'xxxxxxx';	//注:这里需使用自己的token
            this.cesium.viewer = new Cesium.Viewer("cesiumContainer", {
                useDefaultRenderLoop: false,    //关闭自动渲染循环,这意味着需要手动调用render()方法来渲染场景
                selectionIndicator: false,  //关闭选中指示器
                homeButton: false,  //关闭回到初始位置按钮
                sceneModePicker: false, //关闭场景模式选择器
                navigationHelpButton: false,    //表示关闭导航帮助按钮
                animate: false, //关闭动画
                timeline: false,    //关闭时间线
                fullscreenButton: false,    //关闭全屏按钮
                navigationInstructionsInitiallyVisible: false,
                allowTextureFilterAnisotropic: false,
                contextOptions: {   //配置WebGL上下文选项
                    webgl: {
                        alpha: false,
                        antialias: true,
                        preserveDrawingBuffer: true,
                        failIfMajorPerformanceCaveat: false,
                        depth: true,
                        stencil: false,
                        anialias: false
                    }
                },
                targetFrameRate: 60,    //目标帧率
                resolutionScale: 0.1,   //场景分辨率的缩放比例
                orderIndependentTranslucency: true, 
                baseLayerPicker: true,  //启用底图选择器
                geocoder: false,    //关闭地名查找功能
                automaticallyTrackDataSourceClocks: false,  //关闭自动跟踪数据源时钟
                dataSources: null,
                clock: null,
                terrainShadows: Cesium.ShadowMode.DISABLED, //关闭地形阴影
                infoBox: false  //关闭信息框
            });
            //设置场景中心点的经纬度和高度,将相机飞到该点
            let center = Cesium.Cartesian3.fromDegrees(
                (this.minWGS84[0] + this.maxWGS84[0]) / 2,
                ((this.minWGS84[1] + this.maxWGS84[1]) / 2) - 1,
                200000
            );
            this.ce = center;
            //将相机飞到指定位置
            this.cesium.viewer.camera.flyTo({
                destination: center,    //相机位置
                orientation: {  //相机朝向
                    heading: Cesium.Math.toRadians(0),
                    pitch: Cesium.Math.toRadians(-60),
                    roll: Cesium.Math.toRadians(0)
                },
                duration: 3 //飞行动画的时长
            });
        },
        initThree() {
            let ThreeContainer = document.getElementById("ThreeContainer");
            let fov = 45;
            let width = window.innerWidth;
            let height = window.innerHeight;
            let aspect = width / height;
            let near = 1;
            let far = 10 * 1000 * 1000; // needs to be far to support Cesium's world-scale rendering
            this.three.scene = new THREE.Scene();    //场景
            this.three.camera = new THREE.PerspectiveCamera(fov, aspect, near, far); //透视相机
            this.three.renderer = new THREE.WebGLRenderer({  //渲染器
                alpha: true //true,代表透明度为0,完全透明(渲染器设置背景为透明,达成叠加效果)
            });
            ThreeContainer.appendChild(this.three.renderer.domElement);
        },
        Object3D(mesh, minWGS84, maxWGS84) {
            this.threeMesh = mesh;  //three网格对象
            this.minWGS84 = minWGS84;   //位置边界框最小坐标
            this.maxWGS84 = maxWGS84;   //位置边界框最大坐标
        },
        init3DObject() {
            //创建three球体
            let geometry1 = new THREE.SphereGeometry(1, 32, 32);
            let sphere = new THREE.Mesh(geometry1, new THREE.MeshPhongMaterial({
                color: 0xffffff,
                side: THREE.DoubleSide
            })); 
            sphere.scale.set(5000, 5000, 5000); //网格模型xyz方向都缩放5000倍
            sphere.uuid = "sphere";
            //创建组对象group
            var group = new THREE.Group();
            group.add(sphere);  //把球体添加到组
            this.three.scene.add(group); //把组添加到场景中
            group.position.set(this.ce.x, this.ce.y, this.ce.z);   //设置组的位置
            let ob3D = new this.Object3D(group, this.minWGS84, this.maxWGS84);
            this.objects3D.push(ob3D);
            //创建three十二面体
            let geometry2 = new THREE.DodecahedronGeometry();
            let dodecahedronMesh = new THREE.Mesh(geometry2, new THREE.MeshNormalMaterial()); 
            dodecahedronMesh.scale.set(5000, 5000, 5000);
            dodecahedronMesh.position.z += 15000;
            dodecahedronMesh.rotation.x = Math.PI / 2; // Three.js 渲染 z-up 而 Cesium 渲染 y-up,使three也变成y朝上
            dodecahedronMesh.uuid = "12面体";
            var group2 = new THREE.Group();
            group2.add(dodecahedronMesh)
            this.three.scene.add(group2); 
            group2.position.set(this.ce.x, this.ce.y, this.ce.z);
            //添加到对象数组
            let ob3D2 = new this.Object3D(group2, this.minWGS84, this.maxWGS84);
            this.objects3D.push(ob3D2);
            console.log(this.objects3D);
            /*******************************************添加灯光**********************************/
            //添加点光源
            var spotLight = new THREE.SpotLight(0xffffff);
            spotLight.position.set(0, 0, 50000);
            spotLight.castShadow = true; //设置光源投射阴影
            spotLight.intensity = 1;
            group.add(spotLight)
            //添加环境光
            var hemiLight = new THREE.HemisphereLight(0xff0000, 0xff0000, 1);
            group.add(hemiLight);
        },
        loop() {
            requestAnimationFrame(this.loop);
            this.renderCesium(); //渲染cesium
            this.renderThreeObj();   //渲染three
        },
        renderCesium() {
            this.cesium.viewer.render();
        },
        renderThreeObj() {
            let Cesium = this.Cesium;
            let ThreeContainer = document.getElementById("ThreeContainer");
            // 设置相机跟cesium保持一致,使用的cesium的相机为主相机,使three的相机与cesium保持一致即可
            this.three.camera.fov = Cesium.Math.toDegrees(this.cesium.viewer.camera.frustum.fovy) 
            this.three.camera.updateProjectionMatrix();

            // 笛卡尔坐标转换为三维向量
            var cartToVec = function(cart) {
                return new THREE.Vector3(cart.x, cart.y, cart.z);
            };
            // 配置three对象的位置,进行坐标变换才能使对象在地球上正确显示
            for (let id in this.objects3D) {
                let minWGS84 = this.objects3D[id].minWGS84;
                let maxWGS84 = this.objects3D[id].maxWGS84;
                // 物体中心位置计算为对象的最小和最大WGS84纬度和经度值的平均值,并且把经纬度坐标(WGS84)转笛卡尔坐标
                var center = Cesium.Cartesian3.fromDegrees((minWGS84[0] + maxWGS84[0]) / 2, (minWGS84[1] + maxWGS84[1]) / 2);

                // 向前方向计算为高度为1的Cartesian3位置,以便对象指向远离地球中心的方向
                var centerHigh = Cesium.Cartesian3.fromDegrees((minWGS84[0] + maxWGS84[0]) / 2, (minWGS84[1] + maxWGS84[1]) / 2, 1);

                // 使用 WGS84 区域从左下角到左上角的方向作为向上矢量
                var bottomLeft = cartToVec(Cesium.Cartesian3.fromDegrees(minWGS84[0], minWGS84[1]));
                var topLeft = cartToVec(Cesium.Cartesian3.fromDegrees(minWGS84[0], maxWGS84[1]));
                var latDir = new THREE.Vector3().subVectors(bottomLeft, topLeft).normalize();

                // 物体位置调整
                this.objects3D[id].threeMesh.position.copy(center); //位置设置为中心位置
                //_3Dobjects[id].threeMesh.lookAt(centerHigh);    // threejs-r87版本 
                //threejs-r87以上版本,需改写成如下
                this.objects3D[id].threeMesh.lookAt(centerHigh.x, centerHigh.y, centerHigh.z);    
                this.objects3D[id].threeMesh.up.copy(latDir);   //网格的向上矢量设置为计算出的向上矢量方向
            }

            //关闭相机自动更新
            this.three.camera.matrixAutoUpdate = false;
            // cesium相机位置
            var cvm = this.cesium.viewer.camera.viewMatrix;
            var civm = this.cesium.viewer.camera.inverseViewMatrix;
            //NOTE:r87后版本,threejs源码中相机的lookat重新调整了矩阵,需要将原来放在相机设置参数前的这行代码上移到此处
            three.camera.lookAt(new THREE.Vector3(0, 0, 0));
            // 同步Three相机位置设置
            this.three.camera.matrixWorld.set(
                civm[0], civm[4], civm[8], civm[12],
                civm[1], civm[5], civm[9], civm[13],
                civm[2], civm[6], civm[10], civm[14],
                civm[3], civm[7], civm[11], civm[15]
            );
            this.three.camera.matrixWorldInverse.set(
                cvm[0], cvm[4], cvm[8], cvm[12],
                cvm[1], cvm[5], cvm[9], cvm[13],
                cvm[2], cvm[6], cvm[10], cvm[14],
                cvm[3], cvm[7], cvm[11], cvm[15]
            );
            // three.camera.lookAt(new THREE.Vector3(0, 0, 0));// threejs-r87版本 ,r87以后版本需要上移
            // 相机设置参数
            var width = ThreeContainer.clientWidth;
            var height = ThreeContainer.clientHeight;
            var aspect = width / height;
            this.three.camera.aspect = aspect;
            this.three.camera.updateProjectionMatrix();  // 相机参数更新

            this.three.renderer.setSize(width, height);
            this.three.renderer.render(this.three.scene, this.three.camera);
        }
    },
    mounted () {
        this.initCesium();
        this.initThree();
        this.init3DObject();
        this.loop();
    }
}
</script>

<style scoped>
#cesiumContainer {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    margin: 0;
    overflow: hidden;
    padding: 0;
    font-family: sans-serif;
}
#ThreeContainer {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    margin: 0;
    overflow: hidden;
    padding: 0;
    font-family: sans-serif;
    /* 关闭three鼠标控制器 */
    pointer-events: none;   
}
</style>

结语

🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下博主~

Logo

前往低代码交流专区

更多推荐