Skip to content
Cesium轨迹回放

需求

需要实现如下图所示

思路

  1. 先获取得到的轨迹线,把起点和终点绘制出来,以透明度 30%的形式绘制
  2. 获取该轨迹线人物的头像
  3. 再通过 Cesium 中 entity 自带的# PathGraphics就可以实现轨迹线的模块

Pasted image 20241014142034

代码实现

entityPath实现

这种方法是最简单的方法,但是存在问题如下

问题

  1. 先绘制的entity轨迹线有depthMaterial,可以无视地形,但是path绘制的轨迹线无法无视地形(Cesium官方还没提供接口)
  2. 如果数据量大可能会存在卡顿的问题

解决方法

通过primitive来再做一个轨迹回放,这样深度控制的权力可以掌握在自己手上,而且性能问题也能有比较好的解决

优点

  1. 写法简单快捷,代码量相对少

坑点

因为轨迹是根据时间来的,轨迹终点的时间都是确定的,如果Cesium. ClockRange用默认的UNBOUNDED,就会导致时钟始终往前,就会导致轨迹线path消失。需要把ClockRange设置成CLAMPED,当clock的stoptime到达后不再推进时间,才能解决这个问题 viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;

javascript
// 创建 Cesium Viewer

const viewer = new Cesium.Viewer('cesiumContainer', {

    imageryProvider: new Cesium.IonImageryProvider({ assetId: 3 }),

});

  

const dataOption = {

    userId: '123',

    userName: '测试',

    startTime: '2024-10-12T15:18:39+08:00',

    endTime: '2024-10-14 15:18:39',

    billboardUrl: 'https://sandcastle.cesium.com/images/Cesium_Logo_overlay.png',

    speed: '1',

    color: '#007CFF',

    needPlayback: false,

    maxCount: 8000,

    isZoomTo: false,

    isShow: true,

    uploadSourceType: 1,

};

  

// 轨迹数据

const lineData = [

    [94.9952697710716, 43.68696250150549],

    [94.99493266015436, 43.68663049749048],

    [94.99459555296879, 43.68629849248484],

    [94.99425844951485, 43.68596648648851],

];

  

// 创建轨迹线

const polyline = viewer.entities.add({

    polyline: {

        positions: Cesium.Cartesian3.fromDegreesArray(

            lineData.flat() // 将二维数组转换为一维数组

        ),

        width: 5,

        material: Cesium.Color.fromAlpha(Cesium.Color.fromCssColorString('#007CFF'), 0.3),

        clampToGround: true,

    },

});

  

// 模型沿着轨迹移动

const moveData = lineData.map((coord, index) => ({

    time: Cesium.JulianDate.addSeconds(

        Cesium.JulianDate.fromDate(new Date('2024-10-12T15:18:39+08:00')),

        index * 10, // 每隔10秒一个点

        new Cesium.JulianDate()

    ),

    x: coord[0],

    y: coord[1],

    z: 0,

}));

  

console.log('moveData', moveData);

  

// 定义SampledPositionProperty来储存时间和位置

const property = new Cesium.SampledPositionProperty();

moveData.forEach((item) => {

    const position = Cesium.Cartesian3.fromDegrees(item.x, item.y, item.z);

    property.addSample(item.time, position);

});

  

// 设置插值方法

property.setInterpolationOptions({

    interpolationDegree: 2,

    interpolationAlgorithm: Cesium.LagrangePolynomialApproximation,

});

  

// 添加移动的实体

const entityTest = viewer.entities.add({

    availability: new Cesium.TimeIntervalCollection([

        new Cesium.TimeInterval({

            start: moveData[0].time,

            stop: moveData[moveData.length - 1].time,

        }),

    ]),

    position: property,

    billboard: {

        image: 'https://sandcastle.cesium.com/images/Cesium_Logo_overlay.png', // 使用你的billboardUrl

        scale: 0.5,

        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,

    },

    path: {

        leadTime: 0,

        resolution: 1,

        trailTime: 99999999, // 设置为Infinity以保持路径不消失

        material: Cesium.Color.fromCssColorString('#007CFF'),

        width: 5,

    },

});

  

// 设置时间轴

viewer.clock.startTime = moveData[0].time.clone();

viewer.clock.stopTime = moveData[moveData.length - 1].time.clone();

viewer.clock.currentTime = moveData[0].time.clone();

viewer.clock.shouldAnimate = true;

viewer.clock.multiplier = 3; // 这里可以调整为你想要的倍速

  

// 缩放到轨迹线

viewer.zoomTo(viewer.entities);

  

console.log('entityTest', entityTest);

Entity中polyline实现,通过Cesium.CallbackProperty 来进行点位更新

使用 Cesium.CallbackProperty 来进行点位更新,我们可以通过动态计算折线的点位并将其作为回调来生成多段的折线。Cesium.CallbackProperty 允许动态更新属性值,这样折线就可以随着实体的移动而更新。

为什么还是要用entity来实现

因为和项目组需要轨迹线需要透视,即无视深度测试,然后原来写的轨迹线又是使用entity来实现的,然后entity中原来的polyline中本身就有depthFailMaterial,当线被地形或倾斜遮挡的时候仍然可以看到线。

但是entity中path是没有的,所以只能用回polyline来实现这个模块

primitive的话写起来比较复杂,项目上还是不要搞太复杂,之后封装可以使用primitive进行封装

踩坑

Cesium 用Entity绘制polyline,如果使用CallbackProperty方法进行动态绘制,depthFailMaterial属性将失效。!!!

depthFailMaterial for dynamic polylines

代码

javascript
// 创建 Cesium Viewer
const viewer = new Cesium.Viewer('cesiumContainer', {
    imageryProvider: new Cesium.IonImageryProvider({ assetId: 3 }),
});

// 数据参数
const dataOption = {
    userId: '123',
    userName: '测试',
    startTime: '2024-10-12T15:18:39+08:00',
    endTime: '2024-10-14 15:18:39',
    billboardUrl: 'https://sandcastle.cesium.com/images/Cesium_Logo_overlay.png',
    speed: '1',
    color: '#007CFF',
    needPlayback: false,
    maxCount: 8000,
    isZoomTo: false,
    isShow: true,
    uploadSourceType: 1,
};


// 轨迹数据
const lineData = [
    [94.9952697710716, 43.68696250150549],
    [94.99493266015436, 43.68663049749048],
    [94.99459555296879, 43.68629849248484],
    [94.99425844951485, 43.68596648648851],
];

//创建轨迹线

const polyline = viewer.entities.add({

    polyline: {

        positions: Cesium.Cartesian3.fromDegreesArray(

            lineData.flat() // 将二维数组转换为一维数组

        ),

        width: 5,

        material: Cesium.Color.fromAlpha(Cesium.Color.fromCssColorString('#007CFF'), 0.3),

        clampToGround: true,

    },

});

// 模型沿着轨迹移动的时间和坐标点数据
const moveData = lineData.map((coord, index) => ({
    time: Cesium.JulianDate.addSeconds(
        Cesium.JulianDate.fromDate(new Date('2024-10-12T15:18:39+08:00')),
        index * 10, // 每隔10秒一个点
        new Cesium.JulianDate()
    ),
    x: coord[0],
    y: coord[1],
    z: 0,
}));

// 定义SampledPositionProperty来存储时间和位置
const property = new Cesium.SampledPositionProperty();
moveData.forEach((item) => {
    const position = Cesium.Cartesian3.fromDegrees(item.x, item.y, item.z);
    property.addSample(item.time, position);
});

// 设置插值方法
property.setInterpolationOptions({
    interpolationDegree: 2,
    interpolationAlgorithm: Cesium.LagrangePolynomialApproximation,
});

// 定义动态回调属性,更新 polyline 的 positions
const polylinePositions = new Cesium.CallbackProperty(function (time, result) {
    // 获取当前时间下的位置信息
    const currentPosition = property.getValue(time);
    if (!currentPosition) {
        return result;
    }
    
    // 获取已走过的路径(历史位置)
    const positions = moveData
        .filter((item) => Cesium.JulianDate.lessThanOrEquals(item.time, time))
        .map((item) => Cesium.Cartesian3.fromDegrees(item.x, item.y, item.z));

    // 将当前位置添加到折线中
    positions.push(currentPosition);
    
    return positions;
}, false);

// 创建 polyline 实体并动态更新位置
const polylineEntity = viewer.entities.add({
    polyline: {
        positions: polylinePositions,
        width: 5,
        material: Cesium.Color.fromAlpha(Cesium.Color.fromCssColorString('#007CFF'), 0.7),
        clampToGround: true,
    },
});

// 添加移动的实体
const entityTest = viewer.entities.add({
    availability: new Cesium.TimeIntervalCollection([
        new Cesium.TimeInterval({
            start: moveData[0].time,
            stop: moveData[moveData.length - 1].time,
        }),
    ]),
    position: property,
    billboard: {
        image: 'https://sandcastle.cesium.com/images/Cesium_Logo_overlay.png',
        scale: 0.5,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
    }
});

// 设置时间轴
viewer.clock.startTime = moveData[0].time.clone();
viewer.clock.stopTime = moveData[moveData.length - 1].time.clone();
viewer.clock.currentTime = moveData[0].time.clone();
viewer.clock.shouldAnimate = true;
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;
viewer.clock.multiplier = 3; // 设置播放倍速

// 缩放到所有实体
viewer.zoomTo(viewer.entities);

console.log('polylineEntity', polylineEntity);
console.log('entityTest', entityTest);

Primitive轨迹回放实现

参考文章

[[【转载】Cesium实现动态绘制轨迹线]]

https://blog.csdn.net/XFIRR/article/details/129385516

depthFailMaterial for dynamic polylines

暂时解决方法动态刷新,remove 再 add

官方五年了都没解决

参杂了entity的代码(省事一些,但是要对比一下性能)

javascript
// 创建 Cesium Viewer
const viewer = new Cesium.Viewer('cesiumContainer', {
    imageryProvider: new Cesium.IonImageryProvider({ assetId: 3 }),
});

// 数据参数
const dataOption = {
    userId: '123',
    userName: '测试',
    startTime: '2024-10-12T15:18:39+08:00',
    endTime: '2024-10-14 15:18:39',
    billboardUrl: 'https://sandcastle.cesium.com/images/Cesium_Logo_overlay.png',
    speed: '1',
    color: '#007CFF',
    needPlayback: false,
    maxCount: 8000,
    isZoomTo: false,
    isShow: true,
    uploadSourceType: 1,
};

// 轨迹数据
const lineData = [
    [94.9952697710716, 43.68696250150549],
    [94.99493266015436, 43.68663049749048],
    [94.99459555296879, 43.68629849248484],
    [94.99425844951485, 43.68596648648851],
];

const polyline = viewer.entities.add({

    polyline: {

        positions: Cesium.Cartesian3.fromDegreesArray(

            lineData.flat() // 将二维数组转换为一维数组

        ),

        width: 5,

        material: Cesium.Color.fromAlpha(Cesium.Color.fromCssColorString('#007CFF'), 0.3),

        clampToGround: true,

    },

});

// 模型沿着轨迹移动的时间和坐标点数据
const moveData = lineData.map((coord, index) => ({
    time: Cesium.JulianDate.addSeconds(
        Cesium.JulianDate.fromDate(new Date('2024-10-12T15:18:39+08:00')),
        index * 10, // 每隔10秒一个点
        new Cesium.JulianDate()
    ),
    x: coord[0],
    y: coord[1],
    z: 0,
}));

// 定义SampledPositionProperty来存储时间和位置
const property = new Cesium.SampledPositionProperty();
moveData.forEach((item) => {
    const position = Cesium.Cartesian3.fromDegrees(item.x, item.y, item.z);
    property.addSample(item.time, position);
});

// 设置插值方法
property.setInterpolationOptions({
    interpolationDegree: 2,
    interpolationAlgorithm: Cesium.LagrangePolynomialApproximation,
});

// 变量用于保存折线的primitive
let candidateLinePrimitive = null;

// 函数:根据更新的点位绘制新的折线primitive
function updatePolyline(viewer, positions) {
    // 如果已经存在折线primitive,移除旧的
    if (candidateLinePrimitive) {
        viewer.scene.primitives.remove(candidateLinePrimitive);
    }

    // 创建新的Primitive折线
    candidateLinePrimitive = viewer.scene.primitives.add(
        new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
                geometry: new Cesium.PolylineGeometry({
                    positions: positions,
                    width: 5,
                    vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT
                })
            }),
            appearance: new Cesium.PolylineMaterialAppearance({
                material: new Cesium.Material({
                    fabric: {
                        type: "Color",
                        uniforms: {
                            color: Cesium.Color.fromCssColorString('#007CFF')
                        }
                    }
                }),
                renderState: {
                    depthTest: {
                        enabled: false  // 禁用深度测试
                    }
                }
            }),
            asynchronous: false  // 立即渲染
        })
    );
}

// 保存事件监听器的引用
const preRenderListener = function (scene, time) {
    const currentPosition = property.getValue(time);
    if (!currentPosition) return;

    // 获取当前时间之前的所有位置
    const positions = moveData
        .filter((item) => Cesium.JulianDate.lessThanOrEquals(item.time, time))
        .map((item) => Cesium.Cartesian3.fromDegrees(item.x, item.y, item.z));

    // 添加当前的最新位置
    positions.push(currentPosition);

    // 更新折线
    updatePolyline(viewer, positions);

    // 检查是否到达终点
    if (Cesium.JulianDate.greaterThanOrEquals(time, moveData[moveData.length - 1].time)) {
        // 移除 preRender 事件监听器
        viewer.scene.preRender.removeEventListener(preRenderListener);
    }
};

// 每帧动态更新折线的点位
viewer.scene.preRender.addEventListener(preRenderListener);

// 添加一个移动的实体
const entityTest = viewer.entities.add({
    availability: new Cesium.TimeIntervalCollection([
        new Cesium.TimeInterval({
            start: moveData[0].time,
            stop: moveData[moveData.length - 1].time,
        }),
    ]),
    position: property,
    billboard: {
        image: 'https://sandcastle.cesium.com/images/Cesium_Logo_overlay.png',
        scale: 0.5,
        heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
    }
});

// 设置时间轴
viewer.clock.startTime = moveData[0].time.clone();
viewer.clock.stopTime = moveData[moveData.length - 1].time.clone();
viewer.clock.currentTime = moveData[0].time.clone();
viewer.clock.shouldAnimate = true;
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;
viewer.clock.multiplier = 3; // 设置播放倍速

// 缩放到所有实体
viewer.zoomTo(viewer.entities);

console.log('entityTest', entityTest);

纯primitive写法,且包括透视线、billboard、贴地线

需要注意的是贴地线只能同步渲染,因为需要获取高度数据后才能进行数据绘制,因此按照这样每一帧渲染,就会出现贴地线绘制不出来的情况,因为每绘制完一次都被清除掉。所以针对贴地线的回流写法需要一直绘制,或者绘制两段以上再做清除,或者做一个primitivecollection把所有线段都绘制进去

javascript
// 创建 Cesium Viewer,使用全球地形
const viewer = new Cesium.Viewer('cesiumContainer', {
    imageryProvider: new Cesium.IonImageryProvider({ assetId: 3 }),
    terrain: Cesium.Terrain.fromWorldTerrain(),
    scene3DOnly: true,
});

// 确保启用贴地多段线
viewer.scene.globe.depthTestAgainstTerrain = true;

// 数据参数
const dataOption = {
    userId: '123',
    userName: '测试',
    startTime: '2024-10-12T15:18:39+08:00',
    endTime: '2024-10-14 15:18:39',
    billboardUrl: 'https://sandcastle.cesium.com/images/Cesium_Logo_overlay.png',
    speed: '1',
    color: '#007CFF',
    needPlayback: false,
    maxCount: 8000,
    isZoomTo: false,
    isShow: true,
    uploadSourceType: 1,
    clampToGround: true, // 是否贴地
};

// 轨迹数据
const lineData = [
    [94.9952697710716, 43.68696250150549],
    [94.99493266015436, 43.68663049749048],
    [94.99459555296879, 43.68629849248484],
    [94.99425844951485, 43.68596648648851],
];

// 将二维数组转换为Cesium.Cartesian3数组
const positions = Cesium.Cartesian3.fromDegreesArray(lineData.flat());

let polylineGeometry;
let polylinePrimitive;
if (dataOption.clampToGround) {
    // 创建GroundPolylineGeometry(贴地线)
    polylineGeometry = new Cesium.GroundPolylineGeometry({
        positions: positions,
        width: 5, // 线条宽度
    });

    // 创建几何实例
    const geometryInstance = new Cesium.GeometryInstance({
        geometry: polylineGeometry,
    });

    // 创建材质
    const polylineMaterial = new Cesium.Material({
        fabric: {
            type: 'Color',
            uniforms: {
                color: Cesium.Color.fromAlpha(Cesium.Color.fromCssColorString('#007CFF'), 0.3), // 半透明蓝色
            },
        },
    });

    // 创建 GroundPolylinePrimitive(贴地多段线)
    polylinePrimitive = new Cesium.GroundPolylinePrimitive({
        geometryInstances: geometryInstance,
        appearance: new Cesium.PolylineMaterialAppearance({
            material: polylineMaterial,
        }),
    });
} else {
    // 创建普通PolylineGeometry
    polylineGeometry = new Cesium.PolylineGeometry({
        positions: positions,
        width: 5, // 线条宽度
        vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT, // 使用材质
    });

    // 创建几何实例
    const geometryInstance = new Cesium.GeometryInstance({
        geometry: polylineGeometry,
    });

    // 创建材质
    const polylineMaterial = new Cesium.Material({
        fabric: {
            type: 'Color',
            uniforms: {
                color: Cesium.Color.fromAlpha(Cesium.Color.fromCssColorString('#007CFF'), 0.3), // 半透明蓝色
            },
        },
    });

    // 创建Primitive
    polylinePrimitive = new Cesium.Primitive({
        geometryInstances: geometryInstance,
        appearance: new Cesium.PolylineMaterialAppearance({
            material: polylineMaterial,
        }),
        asynchronous: false, // 同步渲染
    });
}

// 添加primitive到场景
if (dataOption.clampToGround) {
    viewer.scene.groundPrimitives.add(polylinePrimitive); // 如果是贴地线,添加到groundPrimitives
} else {
    viewer.scene.primitives.add(polylinePrimitive); // 否则添加到普通primitives
}

// 模型沿着轨迹移动的时间和坐标点数据
const moveData = lineData.map((coord, index) => ({
    time: Cesium.JulianDate.addSeconds(
        Cesium.JulianDate.fromDate(new Date('2024-10-12T15:18:39+08:00')),
        index * 10, // 每隔10秒一个点
        new Cesium.JulianDate()
    ),
    x: coord[0],
    y: coord[1],
    z: 0,
}));

// 定义SampledPositionProperty来存储时间和位置
const property = new Cesium.SampledPositionProperty();
moveData.forEach((item) => {
    const position = Cesium.Cartesian3.fromDegrees(item.x, item.y, item.z);
    property.addSample(item.time, position);
});

// 设置插值方法
property.setInterpolationOptions({
    interpolationDegree: 2,
    interpolationAlgorithm: Cesium.LagrangePolynomialApproximation,
});

// 变量用于保存折线的primitive
let candidateLinePrimitiveCollection = viewer.scene.primitives.add(new Cesium.PrimitiveCollection());
let candidateBillboard = viewer.scene.primitives.add(
    new Cesium.BillboardCollection({
        scene: viewer.scene,
    })
);

// 函数:根据更新的点位绘制新的折线primitive
function updatePolyline(viewer, positions) {
    // 如果已经存在折线primitive,移除旧的
    if (candidateLinePrimitiveCollection&&!dataOption.clampToGround) {
            candidateLinePrimitiveCollection.removeAll();
    }

    if (dataOption.clampToGround) {
        // 创建贴地折线Primitive
        candidateLinePrimitiveCollection.add(
            new Cesium.GroundPolylinePrimitive({
                geometryInstances: new Cesium.GeometryInstance({
                    geometry: new Cesium.GroundPolylineGeometry({
                        positions: positions,
                        width: 5,
                        vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT,
                    }),
                }),
                appearance: new Cesium.PolylineMaterialAppearance({
                    material: new Cesium.Material({
                        fabric: {
                            type: 'Color',
                            uniforms: {
                                color: Cesium.Color.fromCssColorString('#007CFF'), // 半透明蓝色
                            },
                        },
                    }),
                    renderState: {
                        depthTest: {
                            enabled: false, // 禁用深度测试
                        },
                    },
                }),
            })
        );
    } else {
        // 创建非贴地折线Primitive
        candidateLinePrimitiveCollection.add(
            new Cesium.Primitive({
                geometryInstances: new Cesium.GeometryInstance({
                    geometry: new Cesium.PolylineGeometry({
                        positions: positions,
                        width: 5,
                        vertexFormat: Cesium.PolylineMaterialAppearance.VERTEX_FORMAT,
                    }),
                }),
                appearance: new Cesium.PolylineMaterialAppearance({
                    material: new Cesium.Material({
                        fabric: {
                            type: 'Color',
                            uniforms: {
                                color: Cesium.Color.fromCssColorString('#007CFF'),
                            },
                        },
                    }),
                    renderState: {
                        depthTest: {
                            enabled: false, // 禁用深度测试
                        },
                    },
                }),
                asynchronous: false, // 立即渲染
            })
        );
    }
}

// 函数:根据当前点位更新billboard
function updateBillboard(position) {
    candidateBillboard.removeAll();
    candidateBillboard.add({
        position: position,
        image: dataOption.billboardUrl,
        scale: 0.5,
        heightReference: dataOption.clampToGround ? Cesium.HeightReference.CLAMP_TO_GROUND : Cesium.HeightReference.NONE,
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
    });
}

// 保存事件监听器的引用
const preRenderListener = function (scene, time) {
    const currentPosition = property.getValue(time);
    if (!currentPosition) return;

    // 获取当前时间之前的所有位置
    const positions = moveData.filter((item) => Cesium.JulianDate.lessThanOrEquals(item.time, time)).map((item) => Cesium.Cartesian3.fromDegrees(item.x, item.y, item.z));

    // 添加当前的最新位置
    positions.push(currentPosition);

    // 更新折线
    updatePolyline(viewer, positions);

    // 更新billboard
    updateBillboard(currentPosition);

    // 检查是否到达终点
    if (Cesium.JulianDate.greaterThanOrEquals(time, moveData[moveData.length - 1].time)) {
        // 移除 preRender 事件监听器
        viewer.scene.preRender.removeEventListener(preRenderListener);
    }
};

// 每帧动态更新折线和billboard的位置
viewer.scene.preRender.addEventListener(preRenderListener);

// 设置时间轴
viewer.clock.startTime = moveData[0].time.clone();
viewer.clock.stopTime = moveData[moveData.length - 1].time.clone();
viewer.clock.currentTime = moveData[0].time.clone();
viewer.clock.shouldAnimate = true;
viewer.clock.clockRange = Cesium.ClockRange.CLAMPED;
viewer.clock.multiplier = 3; // 设置播放倍速

// 根据lineData创建包围盒
function flyToBoundingBox(viewer, lineData) {
    // 获取lineData中的最小和最大经纬度
    let minLon = lineData[0][0],
        maxLon = lineData[0][0];
    let minLat = lineData[0][1],
        maxLat = lineData[0][1];

    lineData.forEach(([lon, lat]) => {
        if (lon < minLon) minLon = lon;
        if (lon > maxLon) maxLon = lon;
        if (lat < minLat) minLat = lat;
        if (lat > maxLat) maxLat = lat;
    });

    // 创建一个矩形(包围盒)
    const rectangle = Cesium.Rectangle.fromDegrees(minLon, minLat, maxLon, maxLat);

    // 飞到该包围盒
    viewer.camera.flyTo({
        destination: rectangle,
        duration: 2, // 动画持续时间
        complete: () => console.log('相机已飞到指定位置'),
    });
}
// 根据lineData飞到轨迹的包围盒
flyToBoundingBox(viewer, lineData);

console.log('Billboard和折线primitive已动态更新');
console.log('viewer',viewer)

Updated at: