返回 导航

Vue.js

hangge.com

Vue.js - 集成Cesium构建三维可视化场景教程6(添加glTF/GLB模型、模型移动效果)

作者:hangge | 2026-07-09 09:00
    在实际业务中,三维模型往往是最直观、信息量也最丰富的表现形式之一,例如设备、车辆、人员的三维呈现与动态展示。本将重点介绍如何在 Vue.js 项目中加载 glTF / GLB 三维模型,并实现模型在三维场景中的移动与朝向变化效果。

六、添加 glTF/GLB 模型、模型移动效果

1,样例代码

    下面样例演示了如何在指定地理位置加载 glTF/GLB 格式的三维模型,并利用 SampledPositionProperty 定义时间序列路径,使模型沿预设路线进行平滑移动,同时通过 VelocityOrientationProperty 自动计算模型朝向以始终指向运动方向。
<template>
    <div class="layer-container">
        <div ref="cesiumContainer" class="cesium-container"></div>

        <!-- 加载状态 -->
        <div v-if="loading" class="loading-overlay">
            <div class="loading-spinner"></div>
            <p>加载三维场景中...</p>
        </div>

        <!-- 工具栏 -->
        <div class="toolbar" v-if="!loading">
            <button @click="addGlbEntity">添加模型</button>
            <button @click="startMove">开始移动</button>
            <button @click="stopMove">停止移动</button>
            <button @click="removeAllEntities">清除所有实体</button>
        </div>
    </div>
</template>

<script>
// 引入 Cesium 核心类
import * as Cesium from "cesium";

export default {
    name: 'CesiumViewer',

    data() {
        return {
            viewer: null,
            loading: true,
            modelEntity: null,
            pathEntity: null,
            positionProperty: null, // SampledPositionProperty
            clockStart: null,
            moveTimer: null,
            moving: false,
            sampleTimeSec: 0
        }
    },

    mounted() {
        // 确保 Cesium 已加载
        if (typeof Cesium === 'undefined') {
            console.error('Cesium 未加载,请检查配置')
            this.loading = false
            return
        }

        // 初始化 Cesium 场景
        this.initCesium()
    },

    beforeDestroy() {
        // 组件销毁时清理资源
        this.destroyCesium()
    },

    methods: {
        // 初始化 Cesium
        async initCesium() {
            try {
                // 配置 Cesium 访问令牌(如果需要,从 Cesium Ion 获取)
                //Cesium.Ion.defaultAccessToken = '你的Cesium Ion访问令牌'

                // 创建 Viewer 实例
                this.viewer = new Cesium.Viewer(this.$refs.cesiumContainer, {
                    animation: false, // 是否显示动画控件
                    baseLayerPicker: false, // 是否显示图层选择控件
                    fullscreenButton: false, // 是否显示全屏按钮
                    geocoder: false, // 是否显示地理编码搜索控件
                    homeButton: false, // 是否显示Home按钮
                    infoBox: false, // 是否显示信息框
                    sceneModePicker: false, // 是否显示3D/2D选择器
                    selectionIndicator: false, // 是否显示选取指示器组件
                    timeline: false, // 是否显示时间轴
                    navigationHelpButton: false, // 是否显示帮助按钮
                    scene3DOnly: true, // 每个几何实例仅以3D渲染以节省GPU内存
                    shouldAnimate: true, // 自动播放动画
                    // 地形配置
                    terrainProvider: await Cesium.createWorldTerrainAsync()
                })

                // 强制重新计算画布尺寸
                this.viewer.canvas.width = this.viewer.container.clientWidth;
                this.viewer.canvas.height = this.viewer.container.clientHeight;
                this.viewer.resize(); // 核心方法:告知 Cesium 容器大小已变

                // 设置初始视图
                this.setInitialView()

                // 加载完成
                this.loading = false

                console.log('Cesium 场景初始化完成')
            } catch (error) {
                console.error('初始化 Cesium 失败:', error)
                this.loading = false
            }
        },

        // 设置初始视图
        setInitialView() {
            // 设置初始位置(例如:北京天安门)
            const initialPosition = Cesium.Cartesian3.fromDegrees(116.3914, 39.9074, 1100)
            this.viewer.camera.setView({
                destination: initialPosition,
                orientation: {
                    heading: Cesium.Math.toRadians(0),
                    pitch: Cesium.Math.toRadians(-90),
                    roll: 0
                }
            })
        },


        // 添加glb模型
        addGlbEntity() {
            this.modelEntity = this.viewer.entities.add({
                name: '模型',
                position: Cesium.Cartesian3.fromDegrees(116.3894, 39.9047, 0),
                model: {
                    uri: '/models/glb/car_type_0.glb',
                    minimumPixelSize: 64, // 在缩放时保持最小像素大小
                    scale: 1.0,
                    incrementallyLoadTextures: true, // 逐步加载贴图
                }
            })
        },

        // 使用 SampledPositionProperty 创建时间序列位置(路径)并让模型沿路径移动
        startMove() {
            if (!this.modelEntity) {
                console.warn('请先加载模型(点击 "加载 3D 模型")。');
                return;
            }

            // 若已存在 positionProperty,清理
            if (this.positionProperty) {
                this.positionProperty = null;
            }

            // 创建 SampledPositionProperty,并填充若干时间点(演示用)
            const positionProperty = new Cesium.SampledPositionProperty();

            // 使用当前时刻作为起点
            const start = Cesium.JulianDate.now();
            this.clockStart = start;

            // 示例路径点(可替换为任意经纬度序列)
            const pathCoords = [
                [116.3894, 39.9047, 0],
                [116.3894, 39.9057, 0],
                [116.3899, 39.9060, 0],
                [116.3928, 39.9060, 0]
            ];

            // 每个点间隔 6 秒
            let seconds = 0;
            for (let i = 0; i < pathCoords.length; i++) {
                const t = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
                const coord = pathCoords[i];
                const cart = Cesium.Cartesian3.fromDegrees(coord[0], coord[1], coord[2]);
                positionProperty.addSample(t, cart);
                seconds += 6;
            }

            // 将 positionProperty 绑定到实体
            this.modelEntity.position = positionProperty;

            // 自动计算朝向(沿速度方向)
            this.modelEntity.orientation = new Cesium.VelocityOrientationProperty(positionProperty);

            // 给实体一个路径可视化(折线) 
            if (this.pathEntity) {
                this.viewer.entities.remove(this.pathEntity);
                this.pathEntity = null;
            }

            // 从 sampled property 取出插值点以构建 polyline positions
            // (这里我们取了固定时间步进行可视化)
            const polylinePositions = [];
            const samplesCount = 60; // 分段数,越大越平滑
            for (let i = 0; i <= samplesCount; i++) {
                const frac = i / samplesCount;
                const secondsOffset = Math.round(seconds * frac);
                const t = Cesium.JulianDate.addSeconds(start, secondsOffset,
                    new Cesium.JulianDate());
                const pos = positionProperty.getValue(t);
                if (pos) polylinePositions.push(pos);
            }

            this.pathEntity = this.viewer.entities.add({
                polyline: {
                    positions: polylinePositions,
                    width: 4,
                    material: new Cesium.PolylineGlowMaterialProperty({
                        glowPower: 0.2,
                        color: Cesium.Color.CYAN
                    }),
                    clampToGround: false
                }
            });

            // 设置时钟范围与速度,便于使用 timeline/animation 控制回放
            const stop = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
            this.viewer.clock.startTime = start.clone();
            this.viewer.clock.currentTime = start.clone();
            this.viewer.clock.stopTime = stop.clone();
            this.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED; // 到达 stopTime 停止
            this.viewer.clock.multiplier = 1; // 真实时间倍率(可调整)
            // 开始播放
            this.viewer.clock.shouldAnimate = true;
            this.moving = true;
            this.positionProperty = positionProperty;
        },

        // 停止模型移动
        stopMove() {
            if (this.viewer) {
                this.viewer.clock.shouldAnimate = false;
                this.moving = false;
            }
        },

        // 清空所有实体
        removeAllEntities() {
            if (!this.viewer) return;
            this.viewer.entities.removeAll();
            this.modelEntity = null;
            this.pathEntity = null;
            this.positionProperty = null;
            this.moving = false;
            // 重置时钟(可选)
            this.viewer.clock.shouldAnimate = false;
        },

        // 销毁 Cesium 资源
        destroyCesium() {
            if (this.viewer) {
                this.viewer.destroy()
                this.viewer = null
            }
        }
    }
}
</script>

<style scoped>
.layer-container {
    position: relative;
    width: 100%;
    height: 100%;
}


.cesium-container {
    width: 100%;
    height: 100%;
}

.loading-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 1000;
    color: white;
}

.loading-spinner {
    border: 4px solid rgba(255, 255, 255, 0.3);
    border-radius: 50%;
    border-top: 4px solid #42b983;
    width: 50px;
    height: 50px;
    animation: spin 1s linear infinite;
    margin-bottom: 15px;
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

.toolbar {
    position: absolute;
    top: 20px;
    left: 20px;
    z-index: 999;
    display: flex;
    gap: 5px;
}

.toolbar button {
    padding: 8px 15px;
    background: rgba(40, 44, 52, 0.8);
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    transition: background 0.3s;
}

.toolbar button:hover {
    background: rgba(66, 185, 131, 0.8);
}
</style>

2,运行效果

(1)点击工具栏“添加模型”按钮会在预设的经纬度位置处加载并显示一个 glTF/GLB 格式的三维车辆模型,模型按照真实地理坐标放置在场景中,并随视角缩放自动调整显示尺寸。

(2)点击工具栏“开始移动”按钮后,模型开始沿预先定义的路径进行平滑移动,移动过程中位置随时间连续更新,同时模型会根据运动方向自动调整朝向,始终保持“头部”指向前进方向;与此同时,场景中会绘制一条发光折线,用于可视化展示模型的运动轨迹,整体效果类似车辆或设备在地图中的动态行进过程。

(3)点击工具栏“停止移动”按钮,模型的运动动画暂停,模型停留在当前时刻对应的位置,路径依然保留在场景中,便于观察模型当前状态和已行进路线。

(4)点击工具栏“清除所有实体”按钮,移除场景中所有已添加的模型和轨迹实体,三维场景恢复为初始状态,方便重新加载模型并再次进行运动演示。

附:旋转三维模型

1,问题描述

(1)有时我们在 Cesium 中将 glb 模型加载进来可能会发现模型出现偏转 90° / 180° / 倒着 / 侧着等问题。例如,上面样例移动时发现吊车朝向有问题,应该逆时针旋转 90°

(2)这是由于模型坐标系 ≠ Cesium 世界坐标系导致的。在 Blender 里,模型看着“朝前”,但导出 glTF 后,Cesium 认为前方是 X 轴。

2,解决办法

(1)对于静态模型(即加载进来后不用移动)只需要用 HeadingPitchRoll 明确告诉 Cesium 模型如何旋转即可。例如想要逆时针旋转 90°
// 添加glb模型
addGlbEntity() {
    const position = Cesium.Cartesian3.fromDegrees(116.3894, 39.9047, 0)

    const orientation =
        Cesium.Transforms.headingPitchRollQuaternion(
            position,
            new Cesium.HeadingPitchRoll(
                Cesium.Math.toRadians(90), // 逆时针 90°
                0,
                0
            )
        )

    this.modelEntity = this.viewer.entities.add({
        name: '模型',
        position,
        orientation,
        model: {
            uri: '/models/glb/car_type_0.glb',
            minimumPixelSize: 64, // 在缩放时保持最小像素大小
            scale: 1.0,
            incrementallyLoadTextures: true, // 逐步加载贴图
        }
    })
},

(2)如果想要模型沿路径移动时,在朝向运动方向此基础上逆时针旋转 90°,那么需要替换我们 startMove() 里的 orientation 设置部分。即将如下部分:
// 自动计算朝向(沿速度方向)
this.modelEntity.orientation = new Cesium.VelocityOrientationProperty(positionProperty);
// 使用 SampledPositionProperty 创建时间序列位置(路径)并让模型沿路径移动
startMove() {
    if (!this.modelEntity) {
        console.warn('请先加载模型(点击 "加载 3D 模型")。');
        return;
    }

    // 若已存在 positionProperty,清理
    if (this.positionProperty) {
        this.positionProperty = null;
    }

    // 创建 SampledPositionProperty,并填充若干时间点(演示用)
    const positionProperty = new Cesium.SampledPositionProperty();

    // 使用当前时刻作为起点
    const start = Cesium.JulianDate.now();
    this.clockStart = start;

    // 示例路径点(可替换为任意经纬度序列)
    const pathCoords = [
        [116.3894, 39.9047, 0],
        [116.3894, 39.9057, 0],
        [116.3899, 39.9060, 0],
        [116.3928, 39.9060, 0]
    ];

    // 每个点间隔 6 秒
    let seconds = 0;
    for (let i = 0; i < pathCoords.length; i++) {
        const t = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
        const coord = pathCoords[i];
        const cart = Cesium.Cartesian3.fromDegrees(coord[0], coord[1], coord[2]);
        positionProperty.addSample(t, cart);
        seconds += 6;
    }

    // 将 positionProperty 绑定到实体
    this.modelEntity.position = positionProperty;

    // 1. 速度方向 orientation
    const velocityOrientation =
        new Cesium.VelocityOrientationProperty(positionProperty)

    // 2. 固定旋转偏移(逆时针 90°)
    const offsetHPR = new Cesium.HeadingPitchRoll(
        Cesium.Math.toRadians(90), // 逆时针 90°
        0,
        0
    )

    // 3. 用 CallbackProperty 组合两个旋转
    this.modelEntity.orientation = new Cesium.CallbackProperty((time) => {
        // 当前速度方向的四元数
        const baseOrientation = velocityOrientation.getValue(time)
        if (!baseOrientation) return baseOrientation

        // 偏移旋转四元数
        const offsetQuat =
            Cesium.Quaternion.fromHeadingPitchRoll(offsetHPR)

        // 最终方向 = 速度方向 × 偏移
        return Cesium.Quaternion.multiply(
            baseOrientation,
            offsetQuat,
            new Cesium.Quaternion()
        )
    }, false)

    // 给实体一个路径可视化(折线) 
    if (this.pathEntity) {
        this.viewer.entities.remove(this.pathEntity);
        this.pathEntity = null;
    }

    // 从 sampled property 取出插值点以构建 polyline positions
    // (这里我们取了固定时间步进行可视化)
    const polylinePositions = [];
    const samplesCount = 60; // 分段数,越大越平滑
    for (let i = 0; i <= samplesCount; i++) {
        const frac = i / samplesCount;
        const secondsOffset = Math.round(seconds * frac);
        const t = Cesium.JulianDate.addSeconds(start, secondsOffset,
            new Cesium.JulianDate());
        const pos = positionProperty.getValue(t);
        if (pos) polylinePositions.push(pos);
    }

    this.pathEntity = this.viewer.entities.add({
        polyline: {
            positions: polylinePositions,
            width: 4,
            material: new Cesium.PolylineGlowMaterialProperty({
                glowPower: 0.2,
                color: Cesium.Color.CYAN
            }),
            clampToGround: false
        }
    });

    // 设置时钟范围与速度,便于使用 timeline/animation 控制回放
    const stop = Cesium.JulianDate.addSeconds(start, seconds, new Cesium.JulianDate());
    this.viewer.clock.startTime = start.clone();
    this.viewer.clock.currentTime = start.clone();
    this.viewer.clock.stopTime = stop.clone();
    this.viewer.clock.clockRange = Cesium.ClockRange.CLAMPED; // 到达 stopTime 停止
    this.viewer.clock.multiplier = 1; // 真实时间倍率(可调整)
    // 开始播放
    this.viewer.clock.shouldAnimate = true;
    this.moving = true;
    this.positionProperty = positionProperty;
},
评论

全部评论(0)

回到顶部