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的融合我也只领略到了皮毛,看了不少优秀博客发布的两者的融合也已经做的足够深入。信息战嘛,了解与未知之间的差距是一条巨大的鸿沟,学无止境,欢迎各位前辈不吝赐教。