Vue3+Cesium实现3DTiles模型实时调节(离地面高度/xyz轴旋转/模型经纬度偏移)

前言

我们在基于 Cesium 开发数字孪生、古建筑展示、三维地图等项目时,经常会导入 3DTiles 格式瓦片模型。相比于普通 glTF/GLB 模型,3DTiles 瓦片模型适配超大场景、高精度古建筑、城市园区等,也是目前三维GIS项目主流使用的模型格式。

但绝大多数开发者都会遇到同一个难题:

在cesium中普通模型(gltf/glb)可以直接改位置、角度,而3DTiles 瓦片模型没有 position/rotation 属性,无法直接修改位置与旋转参数。

针对以上痛点,本文将带大家从零搭建一套可视化控制面板。本文通过可视化面板上可调节的参数,精准控制3DTiles模型的离地面高度、绕XYZ三轴旋转角度以及模型经纬度细微偏移,所有参数修改即时生效,实现所见即所得的调试效果。

演示动态图示如下:

演示地址如下:

3d.xiazhi.tech/3d-demos/#/...

最终实现效果

本案例基于 Vue3 + ElementPlus + Cesium 开发,右侧独立控制面板包含6项核心调节参数,外加两个辅助功能按钮:

  • 模型离地面高度:支持正负数值,正数模型上浮、负数模型下沉
  • X轴旋转:控制模型前后俯仰角度,矫正模型前倾、后仰问题
  • Y轴旋转:控制模型左右倾斜角度,修复模型侧向歪斜
  • Z轴旋转:水平面旋转,用于调整模型朝向
  • 经度偏移:东西方向精细化微调
  • 纬度偏移:南北方向精细化微调
  • 辅助功能:一键重置所有参数、相机复位至模型最佳观测视角

矩阵变换核心原理

Cesium 中所有3DTiles模型调节,底层全部依靠矩阵运算,整体执行流程:

  1. 模型初始化加载,获取模型原始中心点经纬度、高度,作为调节基准值;
  2. 监听控制面板参数变化,实时收集离地面高度、xyz轴旋转、经纬度偏移数据;
  3. 基于ENU(东-北-上)地球坐标系,生成模型平移矩阵、三轴独立旋转矩阵;
  4. 矩阵合并运算,生成最终变换矩阵;
  5. 将最终矩阵赋值给瓦片根节点 _root.transform,实时更新模型状态。

Demo整体结构

整个页面拆分为三大模块,分工明确,解耦易维护:

  1. HTML结构:Cesium渲染容器、功能按钮、参数控制面板;
  2. CSS样式:全局布局、地球容器样式、右侧悬浮控制面板样式;
  3. Script逻辑:Cesium初始化、3DTiles加载、矩阵运算、参数监听、辅助功能。

分步编码实现

编写HTML结构

搭建页面基础布局,使用ElementPlus数字输入框双向绑定参数,参数变更触发修改方法。所有输入框支持正负数值,满足模型多角度、多方位调节需求。

xml 复制代码
<template>
    <div class="main">

        <!-- Cesium 三维场景渲染容器 -->
        <div class="content" ref="content" id="earth"></div>

         <!-- 右侧整体控制面板区域 -->
        <div class="map-control">

            <!-- 功能按钮区:参数重置、视角复位 -->
            <div class="map-btn">
                <el-button type="primary" @click="restart">参数恢复</el-button>
                <el-button type="primary" @click="flyTo">初始位置</el-button>
            </div>

            <!-- 模型参数调节面板 -->
            <div class="model-black">
                <!-- 面板标题 -->
                <div class="model-tt">设计模型</div>

                <!-- 所有参数行布局容器 -->
                <div class="model-row">

                    <!-- 模型离地高度调节 -->
                    <div class="model-row-tt">高度</div>
                    <el-input-number class="model-number-input" v-model="heightVal" @change="changeModel" :step="1"></el-input-number>

                    <!-- X轴旋转调节,限制数值范围 -100 ~ 100 -->
                    <div class="model-row-tt">X轴旋转</div>
                    <el-input-number class="model-number-input" v-model="rxVal" @change="changeModel" :step="1" :min="-100" :max="100"></el-input-number>

                    <!-- Y轴旋转调节,限制数值范围 -100 ~ 100 -->
                    <div class="model-row-tt">Y轴旋转</div>
                    <el-input-number class="model-number-input" v-model="ryVal" @change="changeModel" :step="1" :min="-100" :max="100"></el-input-number>

                    <!-- Z轴旋转调节,无数值范围限制 -->
                    <div class="model-row-tt">Z轴旋转</div>
                    <el-input-number class="model-number-input" v-model="rzVal" @change="changeModel"
                      :step="1"></el-input-number>

                    <!-- 经度方向平移调节,步长0.1,精细微调 -->
                    <div class="model-row-tt">经度平移</div>
                    <el-input-number class="model-number-input" v-model="tLon" @change="changeModel"
                            :step="0.1"></el-input-number>

                    <!-- 纬度方向平移调节,步长0.1,精细微调 -->
                    <div class="model-row-tt">纬度平移</div>
                    <el-input-number class="model-number-input" v-model="tLat" @change="changeModel"
                            :step="0.1"></el-input-number>
                </div>
            </div>
        </div>

    </div>
</template>

Script逻辑实现(逐模块讲解)

1. 初始化响应式变量

定义双向绑定的参数变量、全局实例变量,用于存储模型状态与Cesium实例。

csharp 复制代码
import { onMounted, nextTick, ref, onUnmounted } from 'vue';
import { token } from '../../utils/common.js';
import { ElMessage } from 'element-plus';

// 模型调节参数
let heightVal = ref(0);  // 模型离地面高度
let rxVal = ref(0);  // X轴旋转角度
let ryVal = ref(0);  // Y轴旋转角度
let rzVal = ref(0);  // Z轴旋转角度
let tLon = ref(0);  // 经度偏移量
let tLat = ref(0);  // 纬度偏移量
let params = ref({});  // 全局参数存储

// 模型原始基准经纬度
let longitude = ref(0);
let latitude = ref(0);

2. 初始化Cesium场景

初始化Viewer实例,关闭冗余控件、开启高性能渲染,限制默认可视区域为中国范围,减少资源占用。

less 复制代码
onMounted(() => {
  nextTick(() => {
    initMap();
  });
});

// 初始化Cesium地图
const initMap = () => {

    // 设置Cesium官方授权Token,这里填您的Token
    Cesium.Ion.defaultAccessToken = token;
    
    // 设置相机默认可视范围:仅限中国区域
    Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(89.5, 20.4, 110.4, 61.2);

    // 实例化Viewer,关闭所有无用控件,轻量化场景
    window.viewer = new Cesium.Viewer('earth', {
      animation: false,  // 关闭动画控件
      timeline: false,  // 关闭时间轴
      infoBox: false,  // 关闭信息框
      geocoder: false,  // 关闭地理编码搜索
      homeButton: false,  // 关闭主页按钮
      sceneModePicker: false,  // 关闭场景模式切换
      baseLayerPicker: false,  // 关闭底图选择器
      navigationHelpButton: false,  // 关闭导航帮助
      fullscreenButton: false,  // 关闭全屏按钮
      selectionIndicator: false,  // 关闭选择指示器
      shouldAnimate: false,  // 关闭自动播放动画
      contextOptions: {  // WebGL 上下文配置
        webgl: {
          powerPreference: "high-performance",  // 高性能模式
          preserveDrawingBuffer: false  // 不保留绘图缓冲(节省内存)
        }
      }
    });

    let utc = Cesium.JulianDate.fromDate(new Date('2026/05/02 15:00:00'));

    // 调用加载3DTiles模型的方法
    addModel();

    // 调用相机飞往模型初始位置的方法
    flyTo();

};

3. 加载3DTiles模型并获取基准坐标

加载本案例中古建筑模型的3DTiles瓦片,获取模型包围球中心点经纬度,保存为基准坐标,后续所有偏移调节都基于该坐标计算。

javascript 复制代码
// 加载3DTiles模型
const addModel = async () => {
    try {

        // 加载瓦片模型,并配置加载优化参数
        const tileset = await Cesium.Cesium3DTileset.fromUrl(
            // 注:请将下方的 URL 替换为您的 3D Tiles 模型服务地址
            'YOUR_3D_TILES_URL / tileset.json'
            {
                maximumScreenSpaceError: 48,  // 屏幕空间误差(越小越精细,越大越省性能)
                maximumSimultaneousTileLoads: 16,  // 同时加载瓦片数(默认 8,可适当增加)
                preloadAncestors: false,  // 不预加载祖先瓦片(节省内存)
                preloadSiblings: true,  // 预加载兄弟瓦片(提升流畅度)
                maximumMemoryUsage: 512,  // 【关键】内存上限 512MB(防止爆内存)
                skipLevelOfDetail: true,  // 跳过细节层级(提升加载速度)
                baseScreenSpaceError: 1024  // 基础屏幕空间误差(用于 LOD)
            }
        );

        // 将模型添加至三维场景
        window.tileset = window.viewer.scene.primitives.add(tileset);

        // 获取模型中心点笛卡尔坐标,转换为经纬度
        const cartographic = Cesium.Cartographic.fromCartesian(
          window.tileset.boundingSphere.center
        );
        longitude.value = Cesium.Math.toDegrees(cartographic.longitude);
        latitude.value = Cesium.Math.toDegrees(cartographic.latitude);

        // 初始化模型默认参数
        params.value = {
          tx: longitude.value,
          ty: latitude.value,
          tz: cartographic.height,
          rx: 0,
          ry: 0,
          rz: 0
        };

        // 调初始化模型姿态的方法
        changeModel();
    } catch (err) {
        console.error('3D Tiles 加载失败', err);
    }
};

4. 核心:矩阵合成方法

根据当前面板参数,分别生成xyz旋转矩阵,结合经纬度、离地面高度合成最终变换矩阵,是本案例最核心的代码。

ini 复制代码
    // 合成模型变换矩阵
    const update3dtilesMaxtrix = () => {
        // 1. 将角度转为弧度,生成三轴独立旋转矩阵
        let mx = Cesium.Matrix3.fromRotationX(Cesium.Math.toRadians(params.value.rx));
        let my = Cesium.Matrix3.fromRotationY(Cesium.Math.toRadians(params.value.ry));
        let mz = Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(params.value.rz));

        // 转换为四维矩阵,用于矩阵合并
        let rotationX = Cesium.Matrix4.fromRotationTranslation(mx);
        let rotationY = Cesium.Matrix4.fromRotationTranslation(my);
        let rotationZ = Cesium.Matrix4.fromRotationTranslation(mz);

        // 2. 计算模型最终坐标(基准坐标+偏移量+高度)
        let position = Cesium.Cartesian3.fromDegrees(params.value.tx, params.value.ty, heightVal.value);
        // 3. 创建ENU坐标系基准矩阵
        let m = Cesium.Transforms.eastNorthUpToFixedFrame(position);

        // 4. 矩阵叠加:基准位置 + X/Y/Z三轴旋转
        Cesium.Matrix4.multiply(m, rotationX, m);
        Cesium.Matrix4.multiply(m, rotationY, m);
        Cesium.Matrix4.multiply(m, rotationZ, m);
        return m;
    };

5. 参数监听与模型实时更新

监听输入框参数变化,更新偏移、旋转参数,调用矩阵方法,实时更新模型姿态。经纬度增加阻尼系数,避免偏移幅度过大。

ini 复制代码
// 参数变更,实时更新模型
const changeModel = () => {
    // 更新xyz旋转参数
    params.value.rx = rxVal.value;
    params.value.ry = ryVal.value;
    params.value.rz = rzVal.value;
    // 设置阻尼系数500,实现精细化微调,防止位移过大
    params.value.tx = longitude.value + tLon.value / 500;
    params.value.ty = latitude.value + tLat.value / 500;
    // 赋值给瓦片根节点,刷新模型状态
    window.tileset._root.transform = update3dtilesMaxtrix();
};


/**
 * 模型高度偏移计算方法
 * 作用:单独计算模型垂直方向平移矩阵,实现高度升降
 */
const changeHeight = () => {
   // 1. 获取3DTiles模型包围球中心点,转为经纬度+高度的笛卡尔坐标对象
   const cartographic = Cesium.Cartographic.fromCartesian(
     window.tileset.boundingSphere.center
   );

   // 2. 根据模型中心点经纬度,生成地面基准点(高度为0,贴地位置)
   const surface = Cesium.Cartesian3.fromRadians(
     cartographic.longitude,
     cartographic.latitude
   );

   // 3. 基于同一经纬度,生成偏移高度后的目标坐标点
   const offset = Cesium.Cartesian3.fromRadians(
     cartographic.longitude,
     cartographic.latitude,
     params.value.tz
   );

   // 4. 计算两个坐标点的差值,得到高度偏移向量
   const translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());

   // 5. 将偏移向量转为平移矩阵,赋值给模型
   window.tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);

   // 6. 调用矩阵合成方法,同步更新模型整体姿态
   update3dtilesMaxtrix();
};

6. 辅助功能方法

封装参数重置、相机复位两个辅助方法,方便开发者快速调试。

ini 复制代码
// 一键重置所有参数,恢复模型初始状态
const restart = () => {
  heightVal.value = 0;
  rxVal.value = 0;
  ryVal.value = 0;
  rzVal.value = 0
  tLon.value = 0
  tLat.value = 0
  changeModel();
};

// 相机复位至模型最佳观测视角
const flyTo = () => {
  window.viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(121.43908371916447, 31.349189256570035, 2.5532527089816814),
    orientation: {
      heading: Cesium.Math.toRadians(25.51463348115896),
      pitch: Cesium.Math.toRadians(6.338469775354732),
      roll: Cesium.Math.toRadians(359.99999787825675)
    },
    duration: 6  // 飞行动画时长
  });
};

css样式代码

css 复制代码
* {
    margin: 0;
    padding: 0;
}

.main {
    width: 100%;
    height: 100vh;
    position: relative;
}

.content {
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;
}

.map-control {
    width: 300px;
    height: calc(100vh - 60px);
    position: absolute;
    right: 20px;
    top: 20px;
    z-index: 2;
    display: flex;
    flex-direction: column;
    justify-content: start;
    align-items: start;
    box-sizing: border-box;
}

.map-btn {
    width: 100%;
    display: flex;
    justify-content: end;
    align-items: center;
}

.model-black {
    width: 100%;
    flex: 1;
    min-height: 0;
    margin-top: 30px;
    background-color: rgba(31, 31, 31, 0.8);
    box-sizing: border-box;
    padding: 10px;
}

.model-tt {
    width: 100%;
    font-size: 18px;
    color: #FFF;
    height: 50px;
    line-height: 50px;
    box-sizing: border-box;
    padding-left: 14px;
}

.model-row-tt {
    width: 100%;
    color: #FFF;
    margin-top: 20px;
    font-size: 16px;
    padding-left: 14px;
    box-sizing: border-box;
}

.model-number-input {
    margin-left: 14px;
    margin-top: 14px;
}

参数功能详细解析

模型说明: 文中 3D 城市模型来源于 Sketchfab免费共享库,本人仅作技术演示使用。

相关推荐
zhedream1 小时前
Vue 3 Teleport 报错实录:从 patch 时机到 `defer` 属性
前端·vue.js
雁北向1 小时前
自定义指令 数值输入显示优化 巴飞特 测试
前端·vue.js
用户1733598075371 小时前
花两周用 Vue 3 做了个 PDF 工具站,我在生产环境踩了 8 个坑
前端·vue.js
卤蛋fg61 小时前
使用 vxe-table 树表格实现产品列表与明细关联展示
vue.js
阿猫的故乡1 小时前
Vue自定义指令从入门到实用:自动聚焦、权限控制、防抖、懒加载……全案例教学
前端·javascript·vue.js
该用户已成仙2 小时前
vue3 使用 vuedraggable 报错 TypeError: isFunction2 is not a function
前端·javascript·vue.js
San813_LDD2 小时前
[Vue/HTML]ECharts 使用指南:从入门到绘制各种常用图表
vue.js·html·echarts
智码看视界2 小时前
Vue生态体系:构建现代化前端应用的完整解决方案
前端·javascript·vue.js
仰望.2 小时前
vue表格使用 vxe-table 展开行实现产品列表与明细列表
前端·javascript·vue.js·vxe-table