Cesium 3D 变换控制器教程

📅 2026/6/27 23:44:32
Cesium 3D 变换控制器教程
Cesium 3D 变换控制器 ·Cesium 3D Transform Controls· ▶ 在线运行案例案例合集三维可视化功能案例threehub.cn开源仓库github地址https://github.com/z2586300277/three-cesium-examples400个案例代码:网盘链接你将学到什么Cesium Viewer 初始化与场景配置外部模型 / 3D Tiles 加载Cesium 相机定位与跟随Cesium Primitive 层海量渲染GUI 参数调试面板效果说明本案例演示Cesium 3D 变换控制器效果基于 WebGL 实现「Cesium 3D 变换控制器」可视化效果附完整可运行源码核心用到 3D。建议先打开文首在线案例查看动态画面再对照下方源码逐步理解。核心概念Viewer封装地球、相机、图层与 clock可关闭 animation/timeline 精简 UI。异步 Loader 返回 scene 或 tileset注意 scale、坐标系与modelMatrix贴地。flyTo带动画定位trackedEntity第三人称跟随实体。BillboardCollection / Primitive合批渲染适合万级点面。dat.GUI / lil-gui 绑定 uniform 或配置对象实时调参。实现步骤创建 Viewer配置地形/影像若案例需要并设置初始相机异步加载模型 / 3D Tiles / GeoJSON 等资源并加入 scene 或 entities在requestAnimationFrame循环中更新状态并 renderCesium 为viewer.render或自动渲染代码要点/**Cesium Transform Controls - 模型变换控制器演示本案例演示了 cesium-transform-controls 插件的核心功能* 功能说明1. 支持模型的平移translate、旋转rotate、缩放scale三种变换模式2. 支持局部坐标系local和地表坐标系surface两种坐标模式3. 支持对整个模型Model或模型子节点ModelNode进行变换操作4. 实时显示模型的位置、旋转角度和缩放信息* 依赖- cesium-transform-controls: https://github.com/123164867376464646/cesium-transform-controls* author 123164867376464646*/// 导入模块 import * as Cesium from cesium import { CoordinateMode, Gizmo, GizmoMode } from cesium-transform-controls import { GUI } from dat.guiconsole.log(Cesium.VERSION)// Cesium Viewer 初始化 /**初始化 Cesium Viewer 实例type {Cesium.Viewer}*/// 获取地图容器元素 const box document.getElementById(box)const viewer new Cesium.Viewer(box, { baseLayerPicker: false, // 不显示图层选择器 geocoder: false, // 不显示地理编码器 homeButton: false, // 不显示主页按钮 sceneModePicker: false, // 不显示场景模式选择器 navigationHelpButton: false, // 不显示导航帮助按钮 animation: false, // 不显示动画控件 timeline: false, // 不显示时间线 infoBox: false, // 不显示信息框 })// 模型加载配置 /* 模型基准经度/ const baseLon 106.58446188 /* 模型基准纬度/ const baseLat 29.57088337 /* 模型基准高度/ const baseHeight 0/**异步加载 glTF 模型type {Cesium.Model}*/ const model await Cesium.Model.fromGltfAsync({ url: https://cesium-transform-controls.netlify.app/luaz.glb, modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame( Cesium.Cartesian3.fromDegrees(baseLon, baseLat, baseHeight), new Cesium.HeadingPitchRoll( Cesium.Math.toRadians(0), Cesium.Math.toRadians(0), Cesium.Math.toRadians(0) ), ), scale: 10, }) viewer.scene.primitives.add(model)// Gizmo 控制器初始化 /**创建 Gizmo 变换控制器实例type {Gizmo}*/ const gizmo new Gizmo({ /**Gizmo 拖拽移动时的回调函数用于实时更新 GUI 中显示的坐标信息param {Object} event - 事件对象*/ onGizmoPointerMove: (event) { updateCoordinatesFromMatrix(gizmo._mountedPrimitive) }, }) gizmo.attach(viewer)// GUI 控制面板配置 /**GUI 控制面板设置项*/ const settings { transformMode: translate, // 变换模式translate/rotate/scale translateMode: local, // 坐标模式local/surface enabled: true, // 是否启用 Gizmo // 位置信息显示 longitude: 0.000000, // 经度 latitude: 0.000000, // 纬度 height: 0.00, // 高度米 // 旋转角度显示度 rotateX: 0.00, rotateY: 0.00, rotateZ: 0.00, // 缩放比例显示 scaleX: 1.00, scaleY: 1.00, scaleZ: 1.00, }// 设置 Gizmo 初始模式 gizmo.setMode(settings.transformMode) gizmo.coordinateMode CoordinateMode.local// 创建 GUI 控制面板 const gui new GUI({ name: 变换控制器 })/* 坐标模式控制器引用用于在 scale 模式时禁用/ let translateModeController null /* 保存的坐标模式用于从 scale 模式切换回来时恢复/ let savedCoordinateMode local// 变换模式控制 /**添加变换模式选择器- translate: 平移模式- rotate: 旋转模式- scale: 缩放模式仅支持局部坐标系*/ gui.add(settings, transformMode, [translate, rotate, scale]) .name(变换模式) .onChange((value) { switch (value) { case translate: gizmo.setMode(GizmoMode.translate) break case rotate: gizmo.setMode(GizmoMode.rotate) break case scale: gizmo.setMode(GizmoMode.scale) break } // 缩放模式仅支持局部坐标系需要禁用坐标模式切换 if (value scale) { savedCoordinateMode settings.translateMode gizmo.coordinateMode CoordinateMode.local translateModeController.domElement.style.pointerEvents none translateModeController.domElement.style.opacity 0.5 } else { translateModeController.domElement.style.pointerEvents auto translateModeController.domElement.style.opacity 1 settings.translateMode savedCoordinateMode gizmo.coordinateMode savedCoordinateMode surface ? CoordinateMode.surface : CoordinateMode.local translateModeController.updateDisplay() } })// 坐标模式控制 /**添加坐标模式选择器- local: 局部坐标系相对于模型自身- surface: 地表坐标系相对于地球表面 ENU*/ translateModeController gui.add(settings, translateMode, [local, surface]) .name(坐标模式) .onChange((value) { switch (value) { case local: gizmo.coordinateMode CoordinateMode.local break case surface: gizmo.coordinateMode CoordinateMode.surface break } })// 启用/禁用控制 gui.add(settings, enabled) .name(启用控制器) .onChange((value) { gizmo.setEnabled(value) })// 信息显示面板 const coordsFolder gui.addFolder(变换信息)/* 拾取对象信息/ const pickSettings { modelName: -, modelType: -, }// 添加信息显示控件只读 const nameController coordsFolder.add(pickSettings, modelName).name(名称).listen() const typeController coordsFolder.add(pickSettings, modelType).name(类型).listen() const lonController coordsFolder.add(settings, longitude).name(经度).listen() const latController coordsFolder.add(settings, latitude).name(纬度).listen() const heightController coordsFolder.add(settings, height).name(高度 (m)).listen() const rotateXController coordsFolder.add(settings, rotateX).name(旋转 X (°)).listen() const rotateYController coordsFolder.add(settings, rotateY).name(旋转 Y (°)).listen() const rotateZController coordsFolder.add(settings, rotateZ).name(旋转 Z (°)).listen() const scaleXController coordsFolder.add(settings, scaleX).name(缩放 X).listen() const scaleYController coordsFolder.add(settings, scaleY).name(缩放 Y).listen() const scaleZController coordsFolder.add(settings, scaleZ).name(缩放 Z).listen() coordsFolder.open()// 禁用名称和类型输入框的编辑 nameController.domElement.style.pointerEvents none typeController.domElement.style.pointerEvents none// 模型挂载与相机定位 /**模型加载完成后的处理- 挂载 Gizmo 到模型子节点- 飞行到模型位置*/ model.readyEvent.addEventListener(() { // 定义要操作的子节点名称 // const nodeName wheel_FR_luaz_diffuse_0 // 轮胎节点 const nodeName door_R_luaz_diffuse_0 // 车门节点 const node model.getNode(nodeName)// 将 Gizmo 挂载到模型子节点 // 也可以使用 gizmo.mountToPrimitive(model, viewer) 挂载到整个模型 gizmo.mountToNode(node, model, viewer)// 挂载完成后初始化显示位置信息 updateCoordinatesFromMatrix(gizmo._mountedPrimitive)// 延迟飞行到模型位置等待模型完全加载 setTimeout(() { const boundingSphere model.boundingSphere viewer.camera.flyToBoundingSphere(boundingSphere, { duration: 0, offset: new Cesium.HeadingPitchRange( Cesium.Math.toRadians(-45), Cesium.Math.toRadians(-15), boundingSphere.radius * 3 ), }) }, 1000) })// 工具函数 /**获取挂载对象的类型param {Object} mounted - 挂载的对象returns {string} 对象类型名称*/ function getMountedObjectType(mounted) { if (!mounted) return -// Entity 类型 if (mounted._isEntity) { return Entity }// ModelNode 子节点类型 if (mounted._isNode mounted._node) { return ModelNode }// 3D Tiles 类型 if (mounted.tileset || mounted.content?.tileset) { return 3DTiles }// Model 类型 if (mounted instanceof Cesium.Model) { return Model }// 其他 Primitive 类型 if (mounted.modelMatrix) { return mounted.constructor?.name || Primitive }return Unknown }/**获取挂载对象的名称param {Object} mounted - 挂载的对象returns {string} 对象名称*/ function getMountedObjectName(mounted) { if (!mounted) return -// Entity 名称 if (mounted._isEntity mounted._entity) { const entity mounted._entity return entity.name || entity.id || Entity }// ModelNode 子节点名称 if (mounted._isNode mounted._node) { const node mounted._node return node.name || node._runtimeNode?.name || ModelNode }// 3D Tiles 名称 if (mounted.tileset) { return mounted.tileset._url?.split(/).pop() || 3D Tileset } if (mounted.content?.tileset) { return mounted.content.tileset._url?.split(/).pop() || 3D Tileset }// Model 名称从 URL 提取文件名 if (mounted instanceof Cesium.Model) { const url mounted._resource?._url || mounted._url || const fileName url.split(/).pop()?.split(?)[0] || Model return fileName }// 其他 Primitive 名称 if (mounted.modelMatrix) { return mounted.constructor?.name || Primitive }return - }/**更新挂载对象信息到 GUI通过检查 Gizmo 的可见状态来决定是否显示信息*/ function updateMountedObjectInfo() { const mounted gizmo._mountedPrimitive // 检查任意模式的 Gizmo 是否可见 const isTransVisible gizmo._transPrimitives?._show ?? false const isRotateVisible gizmo._rotatePrimitives?._show ?? false const isScaleVisible gizmo._scalePrimitives?._show ?? false const isGizmoVisible isTransVisible || isRotateVisible || isScaleVisibleif (mounted isGizmoVisible) { pickSettings.modelName getMountedObjectName(mounted) pickSettings.modelType getMountedObjectType(mounted) coordsFolder.show() } else { pickSettings.modelName - pickSettings.modelType - coordsFolder.hide() } }// 通过 preRender 事件监听 Gizmo 的可见状态变化 viewer.scene.preRender.addEventListener(updateMountedObjectInfo)// 初始化显示当前挂载的模型信息 updateMountedObjectInfo()/**从模型矩阵中提取并更新坐标信息包括位置经纬度高度、旋转角度、缩放比例* param {Object} model - 挂载的模型对象*/ function updateCoordinatesFromMatrix(model) { // 从模型矩阵提取位置 const position Cesium.Matrix4.getTranslation(model.modelMatrix, new Cesium.Cartesian3()) const cartographic Cesium.Cartographic.fromCartesian(position)// 检查坐标是否有效 if (!cartographic) { settings.longitude - settings.latitude - settings.height - settings.rotateX - settings.rotateY - settings.rotateZ - settings.scaleX - settings.scaleY - settings.scaleZ - return }// 更新位置信息弧度转角度 const longitude Cesium.Math.toDegrees(cartographic.longitude) const latitude Cesium.Math.toDegrees(cartographic.latitude) const height cartographic.heightsettings.longitude longitude.toFixed(8) settings.latitude latitude.toFixed(8) settings.height height.toFixed(2)// 提取旋转分量 if (model._isNode model._node) { // 对于子节点ModelNode从节点矩阵中提取旋转 const node model._node let mat node.matrix if (!mat node._runtimeNode node._runtimeNode.transform) { mat node._runtimeNode.transform } if (!mat) { mat Cesium.Matrix4.IDENTITY }// 提取旋转矩阵去除缩放影响 const m3 new Cesium.Matrix3() Cesium.Matrix4.getMatrix3(mat, m3)// 归一化列向量以移除缩放 const c0 Cesium.Cartesian3.fromElements(m3[0], m3[1], m3[2]) const c1 Cesium.Cartesian3.fromElements(m3[3], m3[4], m3[5]) const c2 Cesium.Cartesian3.fromElements(m3[6], m3[7], m3[8]) Cesium.Cartesian3.normalize(c0, c0) Cesium.Cartesian3.normalize(c1, c1) Cesium.Cartesian3.normalize(c2, c2)// 重组纯旋转矩阵 const r00 c0.x, r01 c1.x, r02 c2.x const r10 c0.y, r11 c1.y, r12 c2.y const r20 c0.z, r21 c1.z, r22 c2.z// 分解欧拉角顺序Z - Y - X即 R MzMyMx let x 0, y 0, z 0 if (Math.abs(r20) 0.99999) { y Math.asin(-r20) x Math.atan2(r21, r22) z Math.atan2(r10, r00) } else { // 万向锁情况 y Math.PI / 2 * Math.sign(-r20) z 0 x Math.atan2(-r12, r11) }settings.rotateX Cesium.Math.toDegrees(x).toFixed(2) settings.rotateY Cesium.Math.toDegrees(y).toFixed(2) settings.rotateZ Cesium.Math.toDegrees(z).toFixed(2)} else { // 对于根模型Root Model使用 Cesium 提供的 ENU 转换 // Heading Z轴旋转, Pitch Y轴旋转, Roll X轴旋转 const hpr Cesium.Transforms.fixedFrameToHeadingPitchRoll(model.modelMatrix) settings.rotateX Cesium.Math.toDegrees(hpr.roll).toFixed(2) settings.rotateY Cesium.Math.toDegrees(hpr.pitch).toFixed(2) settings.rotateZ Cesium.Math.toDegrees(hpr.heading).toFixed(2) }// 提取缩放分量 let targetMatrix Cesium.Matrix4.IDENTITY if (model._isNode model._node) { const node model._node if (node.matrix) { targetMatrix node.matrix } else if (node._runtimeNode node._runtimeNode.transform) { targetMatrix node._runtimeNode.transform } } else { targetMatrix model.modelMatrix }const scale Cesium.Matrix4.getScale(targetMatrix, new Cesium.Cartesian3())// 对于根模型还需要考虑 model.scale统一缩放因子 let uniformScale 1.0 if (!model._isNode typeof model.scale number) { uniformScale model.scale }settings.scaleX (scale.x * uniformScale).toFixed(2) settings.scaleY (scale.y * uniformScale).toFixed(2) settings.scaleZ (scale.z * uniformScale).toFixed(2) }// 资源清理 /**页面卸载前清理资源*/ window.addEventListener(beforeunload, () { gizmo.detach() viewer.destroy() })完整源码GitHub小结本文提供Cesium 3D 变换控制器完整 Cesium.js 源码与在线 Demo建议先运行案例再改 uniform/参数做二次实验更多 Cesium.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库