cesium 支持的三维模型格式包括GLTF和GLB(二进制glTF文件)。 本文通过使用Entity图元的modelGraphics对象来加载gltf模型,简单对gltf模型的加载进行了封装。通过设置模型的欧拉角,可以计算模型的朝向。
1 3D数学中模型旋转的方式
在3D数学中,有三种方式来旋转模型,即 旋转矩阵、欧拉角、四元数。如何不熟悉,可以先去补一下3D数据基础课程。因为欧拉角比较直观,易与理解,这里简单对其进行介绍。
欧拉角将模型朝向描述为围绕3个垂直轴的3个旋转。在cesium中 定义为“航向-俯仰-滚转”。即模型朝向由航向角(Heading Angle)、俯仰角(PitchAngle)和滚转角(RollAngle)定义。
欧拉角有一个缺陷,即大名鼎鼎的万向节死锁问题。通过四元数可以解决这个问题,所以在实际的计算过程中,一般会将欧拉角转换为四元数后再计算模型朝向。
2 计算模型朝向
cesium 提供了相应的函数来计算模型朝向。
2.1 HeadingPitchRoll 类
该类定义了欧拉角(以弧度表示)来表征旋转。其参数如下:
2.2 Transforms 命名空间
cesium的Transforms命名空间包含一些三维空间的常用的转换函数,其中的headingPitchRollQuaternion函数可以通过模型的位置和设置的欧拉角计算模型的朝向。
2.3 计算模型朝向
calculateOrientation() {
let hpr = new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(this.style.heading),
Cesium.Math.toRadians(this.style.pitch),
Cesium.Math.toRadians(this.style.roll)
);
const position = Cesium.Cartesian3.fromDegrees(this.coords[0], this.coords[1], this.baseHeight)
const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
return orientation
}
3 GltfModel 类封装
ModelGraphics类的构造函数参数如下:
各参数的具体含义请参考官网API,这里不再进行详细说明。
GltfModel.js
/*
* @Description:
* @Author: maizi
* @Date: 2022-05-27 11:36:22
* @LastEditTime: 2023-04-24 09:56:21
* @LastEditors: maizi
*/
const merge = require('deepmerge')
const defaultStyle = {
modelUrl: "",
heading: 0,
pitch: 0,
roll: 0,
scale: 1
}
class GltfModel {
constructor(viewer, coords, options = {}) {
this.viewer = viewer;
this.coords = coords;
this.options = options;
this.props = this.options.props;
this.baseHeight = this.coords[2] || 0;
this.style = merge(defaultStyle, this.options.style || {});
this.entity = null;
this.init();
}
init() {
this.entity = new Cesium.Entity({
id: Math.random().toString(36).substring(2),
type: "gltf_model",
position: Cesium.Cartesian3.fromDegrees(this.coords[0], this.coords[1], this.baseHeight),
props: this.props,
orientation: this.calculateOrientation(),
model: {
uri: this.style.modelUrl,
scale: this.style.scale,
}
});
}
// 计算模型四元数
calculateOrientation() {
//航向
let hpr = new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(this.style.heading),
Cesium.Math.toRadians(this.style.pitch),
Cesium.Math.toRadians(this.style.roll)
);
const position = Cesium.Cartesian3.fromDegrees(this.coords[0], this.coords[1], this.baseHeight)
const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
return orientation
}
updateStyle(style) {
this.style = merge(defaultStyle, style);
this.entity.orientation = this.calculateOrientation();
this.entity.model.scale = this.style.scale;
}
setSelect(enabled) {
if (enabled) {
this.entity.model.silhouetteColor = Cesium.Color.fromAlpha(Cesium.Color.YELLOW, 1);
this.entity.model.silhouetteSize = 4;
} else {
this.entity.model.silhouetteColor = Cesium.Color.fromAlpha(Cesium.Color.YELLOW, 1);
this.entity.model.silhouetteSize = 0;
}
}
}
export {
GltfModel
}
4 完整示例代码
MapWorks.js
import GUI from 'lil-gui';
// 初始视图定位在中国
import { GltfModel } from './GltfModel'
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(90, -20, 110, 90);
//天地图key
const key = '你申请的key'
let viewer = null;
let modelLayer = null
let eventHandler = null
let gltfModelList = []
let selectGraphic = null
let gui = null
function initMap(container) {
viewer = new Cesium.Viewer(container, {
animation: false,
baseLayerPicker: false,
fullscreenButton: false,
geocoder: false,
homeButton: false,
infoBox: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
navigationHelpButton: false,
scene3DOnly: true,
orderIndependentTranslucency: false,
contextOptions: {
webgl: {
alpha: true
}
}
})
viewer._cesiumWidget._creditContainer.style.display = 'none'
viewer.scene.fxaa = true
viewer.scene.postProcessStages.fxaa.enabled = true
if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) {
// 判断是否支持图像渲染像素化处理
viewer.resolutionScale = window.devicePixelRatio
}
// 移除默认影像
removeAll()
// 地形深度测试
viewer.scene.globe.depthTestAgainstTerrain = true
// 背景色
viewer.scene.globe.baseColor = new Cesium.Color(0.0, 0.0, 0.0, 0)
// 太阳光照
viewer.scene.globe.enableLighting = true;
// 初始化图层
initLayer()
// 初始化鼠标事件
initClickEvent()
//调试
window.viewer = viewer
}
function initGui() {
let params = {
...selectGraphic.style
}
gui = new GUI()
let layerFolder = gui.title('样式设置')
layerFolder.add(params, 'heading', -180, 180).step(1.0).onChange(function (value) {
selectGraphic.updateStyle(params)
})
layerFolder.add(params, 'pitch', -90, 90).step(1.0).onChange(function (value) {
selectGraphic.updateStyle(params)
})
layerFolder.add(params, 'roll', -180, 180).step(1.0).onChange(function (value) {
selectGraphic.updateStyle(params)
})
layerFolder.add(params, 'scale',0, 100).step(0.1).onChange(function (value) {
selectGraphic.updateStyle(params)
})
}
function addTdtLayer(options) {
let url = `https://t{s}.tianditu.gov.cn/DataServer?T=${options.type}&x={x}&y={y}&l={z}&tk=${key}`
const layerProvider = new Cesium.UrlTemplateImageryProvider({
url: url,
subdomains: ['0','1','2','3','4','5','6','7'],
tilingScheme: new Cesium.WebMercatorTilingScheme(),
maximumLevel: 18
});
viewer.imageryLayers.addImageryProvider(layerProvider);
}
function initLayer() {
addTdtLayer({
type: 'img_w'
})
addTdtLayer({
type: 'cia_w'
})
modelLayer = new Cesium.CustomDataSource('gltfModel')
viewer.dataSources.add(modelLayer)
}
function loadModels(points) {
points.forEach(item => {
const gltfModel = new GltfModel(viewer, item.coords, {
style:item.style
})
modelLayer.entities.add(gltfModel.entity)
gltfModelList.push(gltfModel)
});
viewer.flyTo(modelLayer)
}
function initClickEvent() {
eventHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
initLeftClickEvent()
initMouseMoveEvent()
}
function initLeftClickEvent() {
eventHandler.setInputAction((e) => {
if (selectGraphic) {
selectGraphic.setSelect(false)
selectGraphic = null
}
if (gui) {
gui.destroy()
}
let pickedObj = viewer.scene.pick(e.position);
if (pickedObj && pickedObj.id) {
if (pickedObj.id.type === 'gltf_model') {
selectGraphic = getGraphicById(pickedObj.id.id)
if (selectGraphic) {
selectGraphic.setSelect(true)
initGui()
}
}
}
},Cesium.ScreenSpaceEventType.LEFT_CLICK)
}
function initMouseMoveEvent() {
eventHandler.setInputAction((e) => {
const pickedObj = viewer.scene.pick(e.endPosition);
if (pickedObj && pickedObj.id) {
if (pickedObj.id.type === 'gltf_model') {
// 改变鼠标状态
viewer._element.style.cursor = "";
document.body.style.cursor = "pointer";
} else {
viewer._element.style.cursor = "";
document.body.style.cursor = "default";
}
} else {
viewer._element.style.cursor = "";
document.body.style.cursor = "default";
}
},Cesium.ScreenSpaceEventType.MOUSE_MOVE)
}
function getGraphicById(id) {
let graphic = null
for (let i = 0; i < gltfModelList.length; i++) {
if (gltfModelList[i].entity.id === id) {
graphic = gltfModelList[i]
break
}
}
return graphic
}
function removeAll() {
viewer.imageryLayers.removeAll();
}
function destroy() {
viewer.entities.removeAll();
viewer.imageryLayers.removeAll();
viewer.destroy();
}
export {
initMap,
loadModels,
destroy
}
GltfModel.vue
<!--
* @Description:
* @Author: maizi
* @Date: 2023-04-07 17:03:50
* @LastEditTime: 2023-04-24 09:53:33
* @LastEditors: maizi
-->
<template>
<div id="container">
</div>
</template>
<script>
import * as MapWorks from './js/MapWorks'
export default {
name: 'GltfModel',
mounted() {
this.init();
},
methods:{
init(){
let container = document.getElementById("container");
MapWorks.initMap(container)
//创建模型点位
let points = [
{
coords: [104.074822, 30.659807],
style: {
modelUrl: '/static/model/xiaofangche.gltf'
}
},
{
coords: [104.074722, 30.659807],
style: {
modelUrl: '/static/model/xiaofangyuan.glb',
}
}
];
MapWorks.loadModels(points)
}
},
beforeDestroy(){
//实例被销毁前调用,页面关闭、路由跳转、v-if和改变key值
MapWorks.destroy();
}
}
</script>
<style lang="scss" scoped>
#container{
width: 100%;
height: 100%;
background: rgba(7, 12, 19, 1);
overflow: hidden;
background-size: 40px 40px, 40px 40px;
background-image: linear-gradient(hsla(0, 0%, 100%, 0.05) 1px, transparent 0), linear-gradient(90deg, hsla(0, 0%, 100%, 0.05) 1px, transparent 0);
}
</style>