原文链接及内容

效果如下图所示:

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<style>
@import url(../templates/bucket.css);

#toolbar {
background: rgba(42, 42, 42, 0.8);
padding: 4px;
border-radius: 4px;
}

#toolbar input {
vertical-align: middle;
padding-top: 2px;
padding-bottom: 2px;
}

#toolbar .header {
font-weight: bold;
}
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay">
<h1>加载中...</h1>
</div>
<div id="toolbar">
<table>
<tbody>
<tr>
<td>发射速率(每秒发射个数)</td>
<td>
<input type="range" min="0.0" max="100.0" step="1" data-bind="value: emissionRate, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: emissionRate">
</td>
</tr>

<tr>
<td>大小(像素)</td>
<td>
<input type="range" min="2" max="60.0" step="1" data-bind="value: particleSize, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: particleSize">
</td>
</tr>

<tr>
<td>最小存活时间(秒)/td>
<td>
<input type="range" min="0.1" max="30.0" step="1"
data-bind="value: minimumParticleLife, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: minimumParticleLife">
</td>
</tr>

<tr>
<td>最大存活时间(秒)</td>
<td>
<input type="range" min="0.1" max="30.0" step="1"
data-bind="value: maximumParticleLife, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: maximumParticleLife">
</td>
</tr>

<tr>
<td>最小速度(m/s)</td>
<td>
<input type="range" min="0.0" max="30.0" step="1" data-bind="value: minimumSpeed, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: minimumSpeed">
</td>
</tr>

<tr>
<td>最大速度(m/s)</td>
<td>
<input type="range" min="0.0" max="30.0" step="1" data-bind="value: maximumSpeed, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: maximumSpeed">
</td>
</tr>

<tr>
<td>起始时缩放</td>
<td>
<input type="range" min="0.0" max="10.0" step="1" data-bind="value: startScale, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: startScale">
</td>
</tr>

<tr>
<td>终止时缩放</td>
<td>
<input type="range" min="0.0" max="10.0" step="1" data-bind="value: endScale, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: endScale">
</td>
</tr>

<tr>
<td>重力(影响)</td>
<td>
<input type="range" min="-20.0" max="20.0" step="1" data-bind="value: gravity, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: gravity">
</td>
</tr>
</tbody>
</table>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
//初始化Viewer对象
const viewer = new Cesium.Viewer("cesiumContainer", {
geocoder: false,
homeButton: false,
sceneModePicker: false,
navigationHelpButton: false,
navigationInstructionsInitiallyVisible: false,
animation: false,
timeline: true,
fullscreenButton: false,
skyBox: false,
shouldAnimate: true,
baseLayerPicker: false,
shadows: true,
});
viewer.cesiumWidget.creditContainer.style.display = "none";

//设置随机数种子(seed,这里翻译为初始值好点)以获得一致的结果。
Cesium.Math.setRandomNumberSeed(3);

//设定模拟时间的界限:以 2015 年 3 月 25 日 16:00:00 为起点,
const start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));
//start 时间加上 120 秒,即即 2015 年 3 月 25 日 16:02:00
const stop = Cesium.JulianDate.addSeconds(start, 120, new Cesium.JulianDate());

viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //一直循环
viewer.clock.multiplier = 1;
viewer.clock.shouldAnimate = true;

//将时间轴设置为上述模拟边界-起止时间
viewer.timeline.zoomTo(start, stop);

//--------------------1.定义例子系统----------------------
const viewModel = {
emissionRate: 5.0, //发射速率,每秒5个例子
gravity: 0.0, //重力影响,0.0表示无重力
// minimumParticleLife与maximumParticleLife:粒子存活时间范围1.2秒
minimumParticleLife: 1.2,
maximumParticleLife: 1.2,
//粒子速度范围1.0~4.0 米/秒
minimumSpeed: 1.0,
maximumSpeed: 4.0,
//粒子从生成到消失的缩放范围(从 1.0 到 5.0 倍)
startScale: 1.0,
endScale: 5.0,
//粒子图像大小(25 像素)
particleSize: 25.0,
};

Cesium.knockout.track(viewModel);
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);

//--------------------2.定义矩阵和变换工具----------------------

/**
* entityPosition, entityOrientation, rotationMatrix, modelMatrix:
* 分别用于存储位置(Cartesian3)、方向(Quaternion)、旋转矩阵(Matrix3)和模型矩阵(Matrix4)
*/
const entityPosition = new Cesium.Cartesian3();
const entityOrientation = new Cesium.Quaternion();
const rotationMatrix = new Cesium.Matrix3();
const modelMatrix = new Cesium.Matrix4();

function computeModelMatrix(entity, time) {
return entity.computeModelMatrix(time, new Cesium.Matrix4());
}

const emitterModelMatrix = new Cesium.Matrix4();
const translation = new Cesium.Cartesian3();
const rotation = new Cesium.Quaternion();
let hpr = new Cesium.HeadingPitchRoll();
const trs = new Cesium.TranslationRotationScale();

/**
*
* 计算粒子发射器的模型矩阵
*/
function computeEmitterModelMatrix() {
//设置发射器的方向(航向、俯仰、滚转均为 0 度)
hpr = Cesium.HeadingPitchRoll.fromDegrees(0.0, 0.0, 0.0, hpr);
//设置发射器相对于模型的平移(向左 4 米,向上 1.4 米)。
trs.translation = Cesium.Cartesian3.fromElements(-4.0, 0.0, 1.4, translation);
// 将方向转换为四元数
trs.rotation = Cesium.Quaternion.fromHeadingPitchRoll(hpr, rotation);
// 结合平移和旋转生成发射器的模型矩阵
return Cesium.Matrix4.fromTranslationRotationScale(trs, emitterModelMatrix);
}

//--------------------3.定义实体和运动轨迹----------------------

// pos1 和 pos2 定义卡车的起点和终点位置(费城附近的地理坐标)
const pos1 = Cesium.Cartesian3.fromDegrees(
-75.15787310614596,
39.97862668312678
);
const pos2 = Cesium.Cartesian3.fromDegrees(
-75.1633691390455,
39.95355089912078
);
// 创建一个采样位置属性,用于插值计算卡车在 start 和 stop 时间之间的位置。
const position = new Cesium.SampledPositionProperty();

// 定义卡车在 start 时间位于 pos1,在 stop 时间位于 pos2,
// Cesium 会自动插值生成平滑路径。
position.addSample(start, pos1);
position.addSample(stop, pos2);

// 添加卡车时间
const entity = viewer.entities.add({
// availability:实体在 start 到 stop 时间段内可见。
availability: new Cesium.TimeIntervalCollection([
new Cesium.TimeInterval({
start: start,
stop: stop,
}),
]),
model: {
uri: "../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb",
minimumPixelSize: 64, //小像素大小为 64 像素,防止模型在远处显得太小
},
// 相机相对于实体的观察偏移(向后 100 米,向上 100 米)
viewFrom: new Cesium.Cartesian3(-100.0, 0.0, 100.0),
position: position,
// 使用 VelocityOrientationProperty 自动根据运动方向调整卡车朝向。
orientation: new Cesium.VelocityOrientationProperty(position),
});
// 设置相机跟踪卡车,使其始终保持在视图中心。
viewer.trackedEntity = entity;

//--------------------4.创建粒子系统----------------------
const scene = viewer.scene;
const particleSystem = scene.primitives.add(
new Cesium.ParticleSystem({
image: "../SampleData/smoke.png", //使用 smoke.png 作为粒子纹理
// startColor / endColor:粒子从浅海绿色(带 0.7 透明度)渐变为白色(完全透明)。
startColor: Cesium.Color.LIGHTSEAGREEN.withAlpha(0.7),
endColor: Cesium.Color.WHITE.withAlpha(0.0),

startScale: viewModel.startScale,
endScale: viewModel.endScale,

minimumParticleLife: viewModel.minimumParticleLife,
maximumParticleLife: viewModel.maximumParticleLife,

minimumSpeed: viewModel.minimumSpeed,
maximumSpeed: viewModel.maximumSpeed,

imageSize: new Cesium.Cartesian2(
viewModel.particleSize,
viewModel.particleSize
),

emissionRate: viewModel.emissionRate,

/**
* bursts:定义粒子爆发事件:
* 在仿真第 5 秒、10 秒、15 秒触发爆发,分别发射 10-100、50-100、200-300 个粒子,创建多彩效果。
*/
bursts: [
// 这些爆裂(burst)偶尔会同步以产生多彩效果
new Cesium.ParticleBurst({
time: 5.0,
minimum: 10,
maximum: 100,
}),
new Cesium.ParticleBurst({
time: 10.0,
minimum: 50,
maximum: 100,
}),
new Cesium.ParticleBurst({
time: 15.0,
minimum: 200,
maximum: 300,
}),
],

lifetime: 16.0,//粒子系统总生命周期为 16 秒

emitter: new Cesium.CircleEmitter(2.0),//使用圆形发射器(半径 2 米)

//粒子发射器的位置和方向(通过 computeEmitterModelMatrix 计算)
emitterModelMatrix: computeEmitterModelMatrix(),

updateCallback: applyGravity,//每帧调用 applyGravity 更新粒子行为
})
);

// --------------------5.自定义重力效果----------------------
const gravityScratch = new Cesium.Cartesian3();//定义一个计算重力方向的暂存值

/**
* 我们需要在地心坐标系中为每个粒子计算一个局部上向量
*/
function applyGravity(p, dt) {
const position = p.position;
// 将粒子当前位置归一化,得到指向地心的单位向量
Cesium.Cartesian3.normalize(position, gravityScratch);
// 根据 viewModel.gravity 和时间步长 dt 计算重力加速度。
Cesium.Cartesian3.multiplyByScalar(
gravityScratch,
viewModel.gravity * dt,
gravityScratch
);
// 将重力加速度累加到粒子速度上,影响粒子运动轨迹。
p.velocity = Cesium.Cartesian3.add(p.velocity, gravityScratch, p.velocity);
}

// --------------------6.场景更新----------------------
// 注册场景更新前的事件监听器
viewer.scene.preUpdate.addEventListener(function (scene, time) {
// 将粒子系统的位置绑定到卡车的模型矩阵,确保粒子系统跟随卡车移动。
particleSystem.modelMatrix = computeModelMatrix(entity, time);

// 更新粒子发射器的位置和方向。
particleSystem.emitterModelMatrix = computeEmitterModelMatrix();

// 如果 viewModel.spin 为 true,
// 则每帧增加发射器的航向、俯仰、滚转角度(各 1 度),使发射器旋转
if (viewModel.spin) {
viewModel.heading += 1.0;
viewModel.pitch += 1.0;
viewModel.roll += 1.0;
}
});

Cesium.knockout
.getObservable(viewModel, "emissionRate")
.subscribe(function (newValue) {
particleSystem.emissionRate = parseFloat(newValue);
});

Cesium.knockout
.getObservable(viewModel, "particleSize")
.subscribe(function (newValue) {
const particleSize = parseFloat(newValue);
particleSystem.minimumImageSize.x = particleSize;
particleSystem.minimumImageSize.y = particleSize;
particleSystem.maximumImageSize.x = particleSize;
particleSystem.maximumImageSize.y = particleSize;
});

Cesium.knockout
.getObservable(viewModel, "minimumParticleLife")
.subscribe(function (newValue) {
particleSystem.minimumParticleLife = parseFloat(newValue);
});

Cesium.knockout
.getObservable(viewModel, "maximumParticleLife")
.subscribe(function (newValue) {
particleSystem.maximumParticleLife = parseFloat(newValue);
});

Cesium.knockout
.getObservable(viewModel, "minimumSpeed")
.subscribe(function (newValue) {
particleSystem.minimumSpeed = parseFloat(newValue);
});

Cesium.knockout
.getObservable(viewModel, "maximumSpeed")
.subscribe(function (newValue) {
particleSystem.maximumSpeed = parseFloat(newValue);
});

Cesium.knockout
.getObservable(viewModel, "startScale")
.subscribe(function (newValue) {
particleSystem.startScale = parseFloat(newValue);
});

Cesium.knockout
.getObservable(viewModel, "endScale")
.subscribe(function (newValue) {
particleSystem.endScale = parseFloat(newValue);
});

const options = [
{
text: "Circle Emitter(圆形发射器)",
onselect: function () {
particleSystem.emitter = new Cesium.CircleEmitter(2.0);//半径2米
},
},
{
text: "Sphere Emitter(球形发射器)",
onselect: function () {
particleSystem.emitter = new Cesium.SphereEmitter(2.5);//半径2.5米
},
},
{
text: "Cone Emitter(锥形发射器)",
onselect: function () {
particleSystem.emitter = new Cesium.ConeEmitter(
Cesium.Math.toRadians(45.0)//45度锥角
);
},
},
{
text: "Box Emitter(立方体发射器)",
onselect: function () {
particleSystem.emitter = new Cesium.BoxEmitter(
new Cesium.Cartesian3(10.0, 10.0, 10.0) //10×10×10米
);
},
},
];

Sandcastle.addToolbarMenu(options);