mcpField
C
cesiumjs-time-properties

cesiumjs-time-properties

github.com/cesiumgs开发193213 次安装6 周前更新
代码仓库

关于 cesiumjs-time-properties

面向 CesiumJS 开发者的技能,介绍时间系统、随时间变化的属性、插值、动画、材质和 CZML,适用于构建模拟与时序可视化。

主要功能

  1. 01JulianDate 与时钟控制
  2. 02时间区间与属性体系
  3. 03采样值与按需计算值
  4. 04位置定向与样条曲线
  5. 05时变材质与路径
  6. 06CZML 时序数据

适用场景

  1. 01沿采样位置为实体制作动画
  2. 02控制播放范围与速度
  3. 03按时间阶段设置路径分段样式
  4. 04从 CZML 加载动态时间数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cesiumgs/cesiumjs-skills --skill cesiumjs-time-properties

安装后在 Claude Code 中输入/cesiumjs-time-properties确认已加载。

详细介绍

CesiumJS 时间、属性与动画

版本基线:CesiumJS v1.144

涵盖时间数据绑定层:Clock/JulianDate 时间系统、让实体属性随时间变化的 Property 层级、插值算法、样条曲线和材质属性。Property 属于此处(而非 Entities),因为 SampledProperty 和 CallbackProperty 若没有 Clock/JulianDate 就毫无意义。Material 类(Fabric)属于 cesiumjs-materials-shaders。

JulianDate -- 时间基元

将整天数和小数秒分开存储,以确保精度。内部始终使用 TAI。

import { JulianDate } from "cesium";

// Creation: fromIso8601 (most common), fromDate, now
const date = JulianDate.fromIso8601("2025-06-15T12:00:00Z");
const jd = JulianDate.fromDate(new Date("2025-06-15T12:00:00Z"));
const now = JulianDate.now();

// Conversion: toIso8601, toDate, toGregorianDate
const iso = JulianDate.toIso8601(date); // "2025-06-15T12:00:00Z"
const greg = JulianDate.toGregorianDate(date); // {year, month, day, hour, ...}

// Arithmetic -- all require a result parameter to avoid allocations
const r = new JulianDate();
JulianDate.addSeconds(date, 3600, r); // also: addMinutes, addHours, addDays

// Differences and comparisons
const stop = JulianDate.addHours(date, 24, new JulianDate());
JulianDate.secondsDifference(stop, date); // 86400
JulianDate.lessThan(date, stop);          // true
JulianDate.compare(date, stop);           // negative (date < stop)

Clock -- 模拟时间控制器

Viewer 会自动创建 Clock。配置它以控制播放速度和时间范围。

import { Viewer, JulianDate, ClockRange, ClockStep } from "cesium";

const viewer = new Viewer("cesiumContainer");
const start = JulianDate.fromIso8601("2025-06-15T00:00:00Z");
const stop = JulianDate.addHours(start, 24, new JulianDate());
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = ClockRange.LOOP_STOP; // loop at end
viewer.clock.multiplier = 60;                   // 60x real-time
viewer.clock.shouldAnimate = true;
viewer.timeline.zoomTo(start, stop);

// Per-frame callback: compute a [0,1] fraction for camera or property animation
viewer.clock.onTick.addEventListener((clock) => {
  const elapsed = JulianDate.secondsDifference(clock.currentTime, clock.startTime);
  const total = JulianDate.secondsDifference(clock.stopTime, clock.startTime);
  const t = Math.max(0, Math.min(1, elapsed / total));
  // Example: interpolate camera position linearly between two points
  // const dest = Cartesian3.lerp(startPos, endPos, t, new Cartesian3());
  // viewer.camera.setView({ destination: dest, orientation: { heading: 0, pitch: CesiumMath.toRadians(-30), roll: 0 } });
});

手动推进时钟 —— 调用 viewer.clock.tick(),在渲染循环之外将时钟推进一帧(适用于在截屏前设置区间中间的状态):

// Advance to midpoint before screenshot
viewer.clock.currentTime = JulianDate.addSeconds(start, 15, new JulianDate());
viewer.clock.tick(); // fires onTick listeners immediately
ClockRange行为
UNBOUNDED向前和向后无限推进
CLAMPED在开始/停止时间处停止
LOOP_STOP从停止时间回绕到开始时间
ClockStep行为
TICK_DEPENDENT每次 tick 按 multiplier 秒推进(取决于帧)
SYSTEM_CLOCK_MULTIPLIER已流逝的实际时间 x multiplier(默认值)
SYSTEM_CLOCK实时时间;忽略 multiplier

TimeInterval 与 TimeIntervalCollection

import { TimeInterval, TimeIntervalCollection, JulianDate } from "cesium";

const interval = TimeInterval.fromIso8601({
  iso8601: "2025-06-15T00:00:00Z/2025-06-16T00:00:00Z",
  data: { phase: "daylight" },  // attach arbitrary data
});
TimeInterval.contains(interval, JulianDate.fromIso8601("2025-06-15T12:00:00Z")); // true

// Used by Entity.availability to cull entities outside the time window
const availability = new TimeIntervalCollection([
  new TimeInterval({
    start: JulianDate.fromIso8601("2025-06-15T00:00:00Z"),
    stop: JulianDate.fromIso8601("2025-06-16T00:00:00Z"),
  }),
]);

Property 系统 -- 随时间变化的值

每个实体属性都是一个 Property。CesiumJS 每帧都会调用 property.getValue(time)。

ConstantProperty

无论时间如何,都返回相同的值。CesiumJS 会自动包装原始值,因此很少需要显式使用。

import { ConstantProperty, Color } from "cesium";
const prop = new ConstantProperty(Color.RED);
prop.setValue(Color.BLUE); // fires definitionChanged
SampledProperty -- 插值时间序列

存储离散样本并进行插值。类型可以是 Number、Cartesian3、Color 或任何 Packable。

import { SampledProperty, JulianDate, LagrangePolynomialApproximation, ExtrapolationType } from "cesium";

const prop = new SampledProperty(Number);
const t0 = JulianDate.fromIso8601("2025-06-15T00:00:00Z");
prop.addSample(t0, 1.0);
prop.addSample(JulianDate.addSeconds(t0, 60, new JulianDate()), 2.5);
prop.addSample(JulianDate.addSeconds(t0, 120, new JulianDate()), 1.0);
prop.getValue(JulianDate.addSeconds(t0, 30, new JulianDate())); // ~1.75

// Default: LinearApproximation degree 1. Switch to smoother Lagrange:
prop.setInterpolationOptions({ interpolationDegree: 5, interpolationAlgorithm: LagrangePolynomialApproximation });
prop.forwardExtrapolationType = ExtrapolationType.HOLD; // hold last value outside range
SampledPositionProperty -- 插值位置

针对 Cartesian3 位置的专用属性。支持参考坐标系(默认是 ReferenceFrame.FIXED,也可以是 INERTIAL)。

import { SampledPositionProperty, JulianDate, Cartesian3, LagrangePolynomialApproximation, ExtrapolationType } from "cesium";

const position = new SampledPositionProperty();
const start = JulianDate.fromIso8601("2025-06-15T00:00:00Z");
for (let i = 0; i <= 360; i += 45) {
  const rad = (i * Math.PI) / 180;
  position.addSample(
    JulianDate.addSeconds(start, i, new JulianDate()),
    Cartesian3.fromDegrees(-112 + 0.045 * Math.cos(rad), 36 + 0.03 * Math.sin(rad), 2000 + Math.random() * 500),
  );
}
position.setInterpolationOptions({ interpolationDegree: 5, interpolationAlgorithm: LagrangePolynomialApproximation });
position.forwardExtrapolationType = ExtrapolationType.HOLD;

对于截屏或评估场景,仅有采样路径往往不够。请在同一个移动实体上添加醒目的标记(point、billboard 或 model),以便在时钟的 currentTime 处识别当前样本。

import { Color } from "cesium";

const aircraft = viewer.entities.add({
  position,
  point: {
    pixelSize: 14,
    color: Color.CYAN,
    outlineColor: Color.BLACK,
    outlineWidth: 2,
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
  },
  path: {
    width: 4,
    leadTime: 180,
    trailTime: 180,
    material: Color.YELLOW,
  },
});
算法最适用场景次数
LinearApproximation快速分段线性插值1(固定)
LagrangePolynomialApproximation根据稀疏样本生成平滑曲线1--9
HermitePolynomialApproximation使用速度导数生成平滑曲线1--9
CallbackProperty -- 按需计算

每帧都会计算一个函数。值会变化时,第二个参数(isConstant)必须为 false。

import { CallbackProperty, Color, JulianDate } from "cesium";

// Pulsing alpha via sine wave
const startTime = JulianDate.now();
const pulse = new CallbackProperty((time, result) => {
  const s = JulianDate.secondsDifference(time, startTime);
  return Color.RED.withAlpha(0.5 + 0.5 * Math.sin(s * 2), result ?? new Color());
}, false);

// Hue cycling -- full color wheel every `period` seconds using Color.fromHsl
// Color.fromHsl(hue 0-1, saturation 0-1, lightness 0-1, alpha 0-1, result?)
const period = 8; // seconds per full cycle
const hueCycle = new CallbackProperty((time, result) => {
  const s = JulianDate.secondsDifference(time, viewer.clock.startTime);
  const hue = (s % period) / period;
  return Color.fromHsl(hue, 0.8, 0.5, 0.8, result ?? new Color());
}, false);
// Use as: polygon.material = new ColorMaterialProperty(hueCycle);

// Growing polygon -- mutate the array, property auto-updates
const pts = [/* initial Cartesian3[] */];
const dynamicPts = new CallbackProperty(() => pts, false);
CompositeProperty -- 按时间拼接属性

针对不同的时间范围委托给不同的子属性。每个区间的 data 都是一个 Property。

import { CompositeProperty, ConstantProperty, SampledProperty, TimeInterval, JulianDate } from "cesium";

const composite = new CompositeProperty();
composite.intervals.addInterval(TimeInterval.fromIso8601({
  iso8601: "2025-06-15T00:00:00Z/2025-06-15T12:00:00Z", data: new ConstantProperty(1.0) }));
const sampled = new SampledProperty(Number);
sampled.addSample(JulianDate.fromIso8601("2025-06-15T12:00:00Z"), 1.0);
sampled.addSample(JulianDate.fromIso8601("2025-06-16T00:00:00Z"), 5.0);
composite.intervals.addInterval(TimeInterval.fromIso8601({
  iso8601: "2025-06-15T12:00:00Z/2025-06-16T00:00:00Z", isStartIncluded: false, data: sampled }));
VelocityOrientationProperty -- 沿路径自动定向

根据位置属性的速度计算 Quaternion。对车辆和飞机至关重要。

import { VelocityOrientationProperty, SampledPositionProperty } from "cesium";
const position = new SampledPositionProperty();
// ... add samples ...
viewer.entities.add({
  position, orientation: new VelocityOrientationProperty(position),
  model: { uri: "aircraft.glb", minimumPixelSize: 64 },
});

为确保稳健的视觉识别,除非确定资源可以加载,否则不要只依赖外部模型 URI。如果提示词要求飞机、卫星或车辆本身可见,请同时添加 point 或 billboard 标记。

ReferenceProperty -- 跨实体绑定

通过 ID 字符串("entityId#propertyPath")将一个实体的属性链接到另一个实体。

import { ReferenceProperty } from "cesium";
viewer.entities.add({ id: "leader", position: Cartesian3.fromDegrees(-75, 40, 1000) });
viewer.entities.add({ id: "follower",
  position: ReferenceProperty.fromString(viewer.entities, "leader#position"),
  point: { pixelSize: 10 } });

加载 CZML 后,这种方式很有用:将 CZML 驱动的路径保留在数据源中,然后添加一个位置引用 CZML 实体的普通 Viewer 实体标记。这样无需重复样本,就能让当前对象清晰可见且易于检查。

材质属性

控制实体的表面外观。所有选项均接受原始值或 Property 实例,以实现随时间变化的行为。表面类型:ColorMaterialProperty、ImageMaterialProperty、GridMaterialProperty、StripeMaterialProperty、CheckerboardMaterialProperty。折线类型:PolylineArrowMaterialProperty、PolylineDashMaterialProperty、PolylineGlowMaterialProperty、PolylineOutlineMaterialProperty。

import { ColorMaterialProperty, SampledProperty, Color, JulianDate } from "cesium";

const solid = new ColorMaterialProperty(Color.RED);

// Time-varying color via SampledProperty
const colorProp = new SampledProperty(Color);
const t0 = JulianDate.fromIso8601("2025-06-15T00:00:00Z");
colorProp.addSample(t0, Color.BLUE);
colorProp.addSample(JulianDate.addHours(t0, 6, new JulianDate()), Color.RED);
const animated = new ColorMaterialProperty(colorProp);

分段路径材质(1.143+)

PathMode.WHOLE 保留原有行为:由当前模拟时间确定的材质为整段可见路径着色。使用 PathMode.PORTIONS,让路径过去和未来的各部分分别使用该部分对应时间的材质着色。

对于离散阶段,优先使用区间材质,因为 Cesium 只会在区间边界处分割:

import {
  Color, ColorMaterialProperty, CompositeMaterialProperty,
  PathMode, TimeInterval,
} from "cesium";

const phases = new CompositeMaterialProperty();
phases.intervals.addInterval(TimeInterval.fromIso8601({
  iso8601: "2026-07-15T12:00:00Z/2026-07-15T12:02:00Z",
  isStopIncluded: false,
  data: new ColorMaterialProperty(Color.LIME),
}));
phases.intervals.addInterval(TimeInterval.fromIso8601({
  iso8601: "2026-07-15T12:02:00Z/2026-07-15T12:04:00Z",
  data: new ColorMaterialProperty(Color.ORANGE),
}));

viewer.entities.add({
  position, // a time-dynamic PositionProperty
  path: {
    material: phases,
    materialMode: PathMode.PORTIONS,
    resolution: 30,
    leadTime: 240,
    trailTime: 240,
    width: 6,
  },
});

materialMode 是一个 Property,而不只是固定枚举。当同一路径需要在模拟过程中切换渲染策略时,请使用随时间变化的模式:

import {
  PathGraphics, PathMode, TimeInterval, TimeIntervalCollectionProperty,
} from "cesium";

const mode = new TimeIntervalCollectionProperty();
mode.intervals.addInterval(TimeInterval.fromIso8601({
  iso8601: "2026-07-15T12:00:00Z/2026-07-15T12:02:00Z",
  isStopIncluded: false,
  data: PathMode.PORTIONS,
}));
mode.intervals.addInterval(TimeInterval.fromIso8601({
  iso8601: "2026-07-15T12:02:00Z/2026-07-15T12:04:00Z",
  data: PathMode.WHOLE,
}));

const path = new PathGraphics({ material: phases, materialMode: mode });
viewer.entities.add({ position, path });

如需进行插值颜色过渡,请将 SampledProperty(Color) 传入 ColorMaterialProperty,并保留 materialMode: PathMode.PORTIONS。Cesium 大约每隔一个 resolution 步长创建分割点。请使用能够保留预期过渡效果的最大步长,并限制 leadTime/trailTime;长时间窗口内使用过小的步长会创建大量折线。

  • 支持正的小数分辨率。在 PORTIONS 模式下,非正分辨率会安全地回退为 60 秒。
  • 对于恒定材质,请保留默认的 WHOLE 模式;分段不会带来额外价值。
  • CesiumJS 1.143 的类型定义在 PathGraphics.ConstructorOptions 中包含 materialMode,但实例成员中没有。对于 TypeScript,请像上面那样在构造函数/实体对象中设置;直接在运行时修改在 JavaScript 中有效,但需要添加范围较窄的本地类型扩展或类型断言。

样条曲线 -- 参数曲线插值

样条曲线使用无单位的参数时间(而非 JulianDate)实现平滑动画曲线。

import { HermiteSpline, CatmullRomSpline, Cartesian3 } from "cesium";

// Natural cubic (C2, auto-tangents)
const spline = HermiteSpline.createNaturalCubic({
  times: [0, 1.5, 3, 4.5, 6],
  points: [
    new Cartesian3(1235398, -4810983, 4146266), new Cartesian3(1372574, -5345182, 4606657),
    new Cartesian3(-757983, -5542796, 4514323), new Cartesian3(-2821260, -5248423, 4021290),
    new Cartesian3(-2539788, -4724797, 3620093) ],
});
const point = spline.evaluate(2.0); // evaluate at parametric time t=2

// CatmullRom (C1, auto-tangents from control points)
const catmull = new CatmullRomSpline({ times: [0, 1, 2, 3], points: [p0, p1, p2, p3] });
样条曲线用途
LinearSpline分段线性(C0),开销最低
HermiteSpline带切线的三次曲线(C1+);工厂方法:createNaturalCubic、createClampedCubic、createC1
CatmullRomSpline根据控制点自动计算切线(C1)
QuaternionSpline通过 SLERP 插值旋转(C1)
ConstantSpline所有时刻的值都相同
SteppedSpline保持当前值,直到下一个控制点
MorphWeightSplineglTF 变形目标权重(C1)

CZML 时间数据

CZML 用于传输随时间变化的数据。document 数据包设置时钟;实体数据包使用 epoch 和偏移数组,以紧凑地表示位置。位置格式:[secondsFromEpoch, lon, lat, alt, ...]。

import { Viewer, CzmlDataSource, Color, ReferenceProperty } from "cesium";

const czml = [
  { id: "document", version: "1.0", clock: {
      interval: "2025-06-15T00:00:00Z/2025-06-15T06:00:00Z",
      currentTime: "2025-06-15T00:00:00Z", multiplier: 60,
      range: "LOOP_STOP", step: "SYSTEM_CLOCK_MULTIPLIER" } },
  { id: "aircraft", availability: "2025-06-15T00:00:00Z/2025-06-15T06:00:00Z",
    position: { epoch: "2025-06-15T00:00:00Z",
      cartographicDegrees: [0,-75,40,10000, 10800,-88,42,11000, 21600,-118,34,9000],
      interpolationAlgorithm: "LAGRANGE", interpolationDegree: 5 },
    point: { pixelSize: 12, color: { rgba: [0,255,255,255] },
             outlineColor: { rgba: [0,0,0,255] }, outlineWidth: 2 },
    path: { width: { number: 3 }, leadTime: { number: 10800 }, trailTime: { number: 10800 },
            material: { solidColor: { color: { rgba: [255,255,0,255] } } } } },
];

const viewer = new Viewer("cesiumContainer", { shouldAnimate: true });
const ds = await CzmlDataSource.load(czml);
viewer.dataSources.add(ds);

// Optional robust marker: references the CZML entity position but lives in viewer.entities.
viewer.entities.add({
  id: "aircraft-marker",
  position: ReferenceProperty.fromString(ds.entities, "aircraft#position"),
  point: {
    pixelSize: 16,
    color: Color.CYAN,
    outlineColor: Color.BLACK,
    outlineWidth: 2,
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
  },
});

await viewer.zoomTo(ds);

CZML 可直接接受枚举名称。若要保留路径上的阶段颜色,请将 path.materialMode 设为 "PORTIONS",并将 path.material 设为带有区间标记的材质数据包数组。保留 "WHOLE" 或省略此字段,即可使用旧版的整条路径行为。

对于卫星轨道的 CZML 截图,请将轨道路径与卫星标记区分开:路径用于证明轨迹,而位于当前 CZML 位置的大型点或广告牌用于标明卫星主体。如果视图为全球视图,请在标记上使用 disableDepthTestDistance: Number.POSITIVE_INFINITY,使其在地球仪背景下仍保持可见。

EasingFunction -- 相机飞行曲线

用于 camera.flyTo 的计时常量(不用于 Property 插值)。常用值:LINEAR_NONE、CUBIC_IN_OUT、QUADRATIC_IN_OUT。完整集合还包括 QUARTIC、QUINTIC、SINUSOIDAL、EXPONENTIAL、CIRCULAR、ELASTIC、BACK、BOUNCE 变体(各自都带有 _IN、_OUT、_IN_OUT)。

import { EasingFunction, Cartesian3 } from "cesium";
viewer.camera.flyTo({
  destination: Cartesian3.fromDegrees(-75, 40, 50000),
  duration: 3.0,
  easingFunction: EasingFunction.CUBIC_IN_OUT,
});

为时变实体设置取景

如果镜头没有对准时变实体,那么它就毫无用处。创建飞行轨迹或轨道后,务必显式设置场景取景——默认 Viewer 相机位于空间中,不会自动缩放到你的实体。截图时有以下三种方式,按推荐顺序排列:

  1. viewer.zoomTo(entityOrDataSource) —— 最佳适配取景;在瓦片集/数据源就绪后返回一个 Promise。适用于 CZML 数据源和一次性本地设置。对于单个移动实体,此方式会将实体当前采样位置的包围球置于画面中,通常会产生近地特写;若要显示长距离路线的完整弧线,建议使用选项 3。
  2. viewer.trackedEntity = entity —— 锁定相机,随实体移动。适合路径跨越大范围距离(跨国飞行、轨道),且希望实体始终位于每一帧画面中央的情况。
  3. viewer.camera.flyTo / setView 配合显式的 Cartesian3.fromDegrees 或 Rectangle.fromDegrees —— 当路径范围已知且默认缩放过宽或过紧时使用(例如,JFK->LAX 航班需要以美国大陆为取景范围,而不是被裁切的机场特写或全球视图)。
// Continental-US framing for a JFK -> LAX flight path
import { Rectangle } from "cesium";
viewer.camera.setView({
  destination: Rectangle.fromDegrees(-130, 20, -60, 50), // west, south, east, north
});

根据路径范围选择取景方式:

路径范围建议的取景方式
本地(城市,<50 km)viewer.zoomTo(entity) 或使用 Cartesian3.fromDegrees(lon, lat, ~5000-50000) 的 setView
区域/大陆(跨国飞行)使用 Rectangle.fromDegrees(...) 的 setView,覆盖两个端点并增加约 5° 的边距
轨道(LEO 卫星,约 90 min 轨道)使用 Cartesian3.fromDegrees(lon, lat, ~20-30 million m) 的 setView,让完整弧线沿可见半球弯曲
长距离且持续跟踪viewer.trackedEntity = entity

对于应完整显示的路径弧线(前导线 + 尾迹),请充分缩小视图,确保 leadTime + trailTime 时长内的运动轨迹都能显示在视口中。如果评审者只能看到弧线的一部分,说明取景范围太紧。对于轨道,将 leadTime 和 trailTime 设置为至少覆盖半个轨道周期(例如,LEO 约为 2700 秒),让弧线明显环绕地球。

对于 JFK 至 LAX 等长距离航班,将时钟设为飞行中途,设置足够大的 leadTime 和 trailTime 以覆盖当前时间附近的路线,添加一个清晰可见的飞机标记,然后使用覆盖美国大陆的矩形。除非提示词要求近距离跟拍,否则不要对这类构图调用 zoomTo(aircraft)。

组合示例:动态飞行

结合 Clock、SampledPositionProperty、VelocityOrientationProperty 和 availability。务必在 path 上设置 leadTime 和 trailTime,以控制当前时间附近可见的尾迹范围,并在截图前显式设置实体取景。

import {
  Viewer, JulianDate, ClockRange, SampledPositionProperty, VelocityOrientationProperty,
  TimeIntervalCollection, TimeInterval, Cartesian3, LagrangePolynomialApproximation, Color,
} from "cesium";

const viewer = new Viewer("cesiumContainer", { shouldAnimate: true });
const start = JulianDate.fromIso8601("2025-06-15T16:00:00Z");
const stop = JulianDate.addSeconds(start, 360, new JulianDate());
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = ClockRange.LOOP_STOP;
viewer.clock.multiplier = 10;
viewer.timeline.zoomTo(start, stop);

const position = new SampledPositionProperty();
for (let i = 0; i <= 360; i += 45) {
  const r = (i * Math.PI) / 180;
  position.addSample(JulianDate.addSeconds(start, i, new JulianDate()),
    Cartesian3.fromDegrees(-112 + 0.045 * Math.cos(r), 36 + 0.03 * Math.sin(r), 2000));
}
position.setInterpolationOptions({ interpolationDegree: 5, interpolationAlgorithm: LagrangePolynomialApproximation });

const aircraft = viewer.entities.add({
  availability: new TimeIntervalCollection([new TimeInterval({ start, stop })]),
  position,
  orientation: new VelocityOrientationProperty(position),
  model: { uri: "aircraft.glb", minimumPixelSize: 64 },
  point: {
    pixelSize: 14,
    color: Color.CYAN,
    outlineColor: Color.BLACK,
    outlineWidth: 2,
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
  },
  path: {
    resolution: 1,
    width: 3,
    leadTime: 180,      // show 3 min of future path
    trailTime: 180,     // show 3 min of past path
    material: Color.YELLOW,
  },
});

// Advance to mid-interval BEFORE framing so the path arc is fully built
viewer.clock.currentTime = JulianDate.addSeconds(start, 180, new JulianDate());
viewer.clock.tick();

// Frame the entity -- without this the camera stays in space and the path is invisible
await viewer.zoomTo(aircraft);
// Or for long-range paths spanning a known region:
// viewer.camera.setView({ destination: Rectangle.fromDegrees(-130, 20, -60, 50) });

对于跨国采样飞行,使用相同的时钟/属性模式,但将本地取景替换为覆盖整条路线的矩形,并确保当前时间标记清晰可见:

import {
  Viewer, JulianDate, ClockRange, SampledPositionProperty, VelocityOrientationProperty,
  TimeIntervalCollection, TimeInterval, Cartesian3, LagrangePolynomialApproximation,
  Color, Rectangle,
} from "cesium";

const viewer = new Viewer("cesiumContainer", { shouldAnimate: true });
const start = JulianDate.fromIso8601("2025-06-15T12:00:00Z");
const stop = JulianDate.addHours(start, 6, new JulianDate());
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = JulianDate.addHours(start, 3, new JulianDate());
viewer.clock.clockRange = ClockRange.LOOP_STOP;
viewer.clock.multiplier = 120;
viewer.timeline.zoomTo(start, stop);

const position = new SampledPositionProperty();
position.addSample(start, Cartesian3.fromDegrees(-73.7781, 40.6413, 10000)); // JFK
position.addSample(JulianDate.addHours(start, 1.5, new JulianDate()), Cartesian3.fromDegrees(-88.0, 41.8, 11500));
position.addSample(JulianDate.addHours(start, 3, new JulianDate()), Cartesian3.fromDegrees(-99.0, 39.0, 12000));
position.addSample(JulianDate.addHours(start, 4.5, new JulianDate()), Cartesian3.fromDegrees(-112.0, 36.5, 11000));
position.addSample(stop, Cartesian3.fromDegrees(-118.4085, 33.9416, 9000)); // LAX
position.setInterpolationOptions({
  interpolationDegree: 5,
  interpolationAlgorithm: LagrangePolynomialApproximation,
});

viewer.entities.add({
  availability: new TimeIntervalCollection([new TimeInterval({ start, stop })]),
  position,
  orientation: new VelocityOrientationProperty(position),
  point: {
    pixelSize: 16,
    color: Color.CYAN,
    outlineColor: Color.BLACK,
    outlineWidth: 2,
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
  },
  path: {
    resolution: 60,
    width: 4,
    leadTime: 10800,
    trailTime: 10800,
    material: Color.YELLOW,
  },
});

viewer.clock.tick();
viewer.camera.setView({
  destination: Rectangle.fromDegrees(-130, 24, -66, 50),
});

性能提示

  1. 对于位置,优先使用 SampledPositionProperty 而非 CallbackProperty —— 二分查找比逐帧回调更快。
  2. 将 interpolationDegree 保持在 5 或以下;稀疏数据使用更高的值可能会出现 Runge 现象。
  3. 在循环中复用 JulianDate 结果参数,以减轻 GC 压力。
  4. 设置实体的 availability,以剔除当前时间范围之外的实体。
  5. 在 CallbackProperty 中返回 result 对象,以避免分配内存。
  6. 使用 CzmlDataSource 加载大量时间数据 —— 它针对批量插入采样进行了优化。
  7. 使用 ExtrapolationType.HOLD,而不是重复添加末尾采样。
  8. 使用 ClockStep.TICK_DEPENDENT 以实现确定性回放;SYSTEM_CLOCK_MULTIPLIER 会随帧率变化。
  9. 尽量减少 CallbackProperty 的数量 —— 每个属性都会逐帧运行其函数。
  10. 优先使用基于区间的 PORTIONS 路径材质;采样材质以平滑过渡为代价,每个 resolution 步长约对应一个线段。
  11. 对于评测截图,视觉上的稳健性很重要:稍大的标记和明确覆盖整条路线的相机设置,优于技术上正确但被裁切、未取景或缺少主体的路径。

时间动态场景的截图检查清单

在为时间动态场景截图之前,请确认:

  1. 时钟位于区间中段 —— 将 viewer.clock.currentTime 设为远离 startTime 的时间,使路径具有可见的尾迹采样,然后调用 viewer.clock.tick()。
  2. 相机对准实体取景 —— 调用 await viewer.zoomTo(entity) 或 viewer.camera.setView({ destination: Rectangle.fromDegrees(...) })。切勿依赖默认的太空视角相机。根据路径范围调整取景(参见上方的取景表):城市飞行使用局部缩放,跨国飞行使用 Rectangle,轨道使用高空视角。
  3. 实体的 path 图形已设置 leadTime 和 trailTime,确保弧线会围绕当前时间绘制。对于轨道,使用足够大的值,至少覆盖半圈轨道,使曲线明显环绕地球。
  4. 移动主体在 currentTime 时可见 —— 为同一实体添加 point、billboard 或已加载的 model。对于全球或轨道视角,应将标记设得足够大,以便看清,并在适当情况下使用 disableDepthTestDistance: Number.POSITIVE_INFINITY。
  5. 实体位于 availability 范围内 —— 时钟的 currentTime 必须落在你设置的任何 TimeIntervalCollection 内,否则实体会被剔除。
  6. 跨国路线使用区域取景 —— JFK 到 LAX 或类似航线应使用 Rectangle.fromDegrees(...) 展示路线全貌,而不是近距离拍摄某个机场附近被裁切的线段。
  7. CZML 主体不能只有路径 —— 加载 CZML 数据源后,确保数据包包含可见的 point/billboard,或使用 ReferenceProperty.fromString(ds.entities, "id#position") 添加查看器实体标记。
  8. 如果希望场景在渲染之间推进,请设置 shouldAnimate: true;否则请手动推进。
  9. 循环变色材质 —— 如果使用驱动 Color.fromHsl 的 CallbackProperty,循环中的任何色相都有效;不要假设截图时会呈现特定色相。必须将时钟推进到 startTime 之后,循环才会从初始色相开始变化。

关键枚举

ClockRange:UNBOUNDED、CLAMPED、LOOP_STOP。ClockStep:TICK_DEPENDENT、SYSTEM_CLOCK_MULTIPLIER、SYSTEM_CLOCK。ExtrapolationType:NONE、HOLD、EXTRAPOLATE。PathMode(v1.143+):WHOLE、PORTIONS。TimeStandard:UTC、TAI。ReferenceFrame:FIXED、INERTIAL。TrackingReferenceFrame(v1.124+):AUTODETECT、ECI、ECEF、INERTIAL、ENU。

另请参阅

  • cesiumjs-entities —— Entity、Graphics 类型、DataSources(属性的使用方)
  • cesiumjs-viewer-setup —— Viewer、ClockViewModel、Timeline 小部件
  • cesiumjs-models-particles —— Model、ModelAnimation(使用时间系统进行播放)
  • cesiumjs-camera —— viewer.zoomTo、viewer.trackedEntity、camera.flyTo、Rectangle.fromDegrees,用于为时间动态场景取景

评论

支持 Markdown