cesiumjs-models-particles
CesiumJS models, glTF, and particle effects - Model, KHR_meshopt_compression, CAD glTF extensions, EdgeDisplayMode, ModelAnimation, ModelNode, ParticleSystem, emitters, GPM extensions. Use when loading compressed or CAD-style glTF/GLB models, controlling edge rendering, playing model animations, positioning particles, or working with geospatial positioning metadata.
How do I install this agent skill?
npx skills add https://github.com/cesiumgs/cesiumjs-skills --skill cesiumjs-models-particlesIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides comprehensive documentation and code examples for using CesiumJS models, glTF extensions, and particle systems. It includes references to official vendor test assets and adheres to standard library practices with no detected security issues.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
CesiumJS Models, glTF & Particle Effects
Version baseline: CesiumJS v1.144.
Quick Reference
| Class | Purpose |
|---|---|
Model | Low-level glTF/GLB primitive; positioned via modelMatrix |
ModelAnimation | Active animation instance on a model |
ModelAnimationCollection | Collection at model.activeAnimations |
ModelNode | Named node with modifiable transform |
ModelFeature | Per-feature styling/picking for feature-ID models |
EdgeDisplayMode | Controls draft glTF edge-visibility rendering on Model/Cesium3DTileset |
ParticleSystem | Billboard-based particle manager (fire, smoke, rain) |
Particle | Single particle with position, velocity, life |
ParticleBurst | Scheduled burst of particles |
BoxEmitter / CircleEmitter | Emit within box volume / flat disk |
ConeEmitter / SphereEmitter | Emit from cone tip / within sphere |
The Entity API exposes models through ModelGraphics (see cesiumjs-entities). The Primitive API uses Model.fromGltfAsync for full control over modelMatrix, animations, and node transforms.
Loading a glTF/GLB Model
Always use the async factory -- never call the constructor directly.
import { Model, Cartesian3, Transforms, HeadingPitchRoll, Math as CesiumMath } from "cesium";
const model = await Model.fromGltfAsync({ url: "path/to/model.glb" });
viewer.scene.primitives.add(model);
Public Sample Models
CesiumJS ships sample models usable without ion tokens:
https://raw.githubusercontent.com/CesiumGS/cesium/main/Apps/SampleData/models/CesiumAir/Cesium_Air.glb
https://raw.githubusercontent.com/CesiumGS/cesium/main/Apps/SampleData/models/CesiumMan/Cesium_Man.glb
https://raw.githubusercontent.com/CesiumGS/cesium/main/Apps/SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb
CesiumJS 1.143 decodes KHR_meshopt_compression automatically, including the
v1 attribute codec and COLOR filter. Do not import a decoder or private loader
helper. When loading compressed glTF, CAD-style lines/points/edges, or
constant-LOD textures, read REFERENCE.md for the complete
support and authoring matrix. The same loader behavior applies to glTF content
inside 3D Tiles.
Positioned Model with Heading
const position = Cartesian3.fromDegrees(-123.074, 44.050, 5000);
const hpr = new HeadingPitchRoll(CesiumMath.toRadians(135), 0, 0);
const model = await Model.fromGltfAsync({
url: "CesiumAir.glb",
modelMatrix: Transforms.headingPitchRollToFixedFrame(position, hpr),
minimumPixelSize: 128, // never smaller than 128 px on screen
maximumScale: 20000, // cap for minimumPixelSize enlargement
scale: 2.0, // uniform scale multiplier
});
viewer.scene.primitives.add(model);
Visual eval framing: for model screenshots, the model must be fully inside the frame and recognizable, not clipped at the bottom edge. Use
minimumPixelSize256-400 for public sample aircraft, add a subtlesilhouetteColor/silhouetteSize, and place the camera with explicit coordinates aimed at the known model position. Avoid shallow pitches that put the model below the frame or rely onviewer.flyTo(model), which is version-sensitive forModelprimitives.
Avoiding Distorted Model Appearance
Models can appear stretched or warped when scale is applied non-uniformly or when the orientation matrix is built incorrectly. Common pitfalls:
- Always use
Transforms.headingPitchRollToFixedFrame(or the Entity API'sheadingPitchRollQuaternion) for ground-aligned orientation. Hand-rolled quaternions often invert pitch/roll axes and produce vertically stretched silhouettes. - Use
scale(uniform) for size, not a non-uniformMatrix4.fromScale. A non-uniform scale baked intomodelMatrixwill distort the model. ReserveMatrix4.fromScalefor per-node tweaks (e.g., stretching a single turret), not the whole model. - Pair
minimumPixelSizewith a sensiblemaximumScale. Without a cap, distant small models can balloon to fill the frame and look elongated when the camera is close.
Key Model.fromGltfAsync Options
| Option | Type | Default |
|---|---|---|
url | string|Resource | required |
modelMatrix | Matrix4 | IDENTITY |
scale | number | 1.0 |
minimumPixelSize | number | 0.0 |
maximumScale | number | -- |
show | boolean | true |
color / colorBlendMode / colorBlendAmount | Color / ColorBlendMode / number | -- / HIGHLIGHT / 0.5 |
edgeDisplayMode | EdgeDisplayMode | SURFACES_ONLY |
silhouetteColor / silhouetteSize | Color / number | RED / 0.0 |
shadows | ShadowMode | ENABLED |
heightReference | HeightReference | NONE |
customShader | CustomShader | -- |
id | any | -- |
allowPicking | boolean | true |
Readiness and Lifecycle
fromGltfAsync resolves once glTF JSON is parsed, but WebGL resources may still load. Wait for readyEvent before accessing animations, nodes, or boundingSphere.
const model = await Model.fromGltfAsync({ url: "robot.glb" });
viewer.scene.primitives.add(model);
model.readyEvent.addEventListener(() => {
console.log("Bounding sphere:", model.boundingSphere);
});
// Synchronous check
if (model.ready) { const bs = model.boundingSphere; }
Animations
Managed through model.activeAnimations (ModelAnimationCollection).
Play by Name / Play All
model.readyEvent.addEventListener(() => {
// Single animation
const anim = model.activeAnimations.add({
name: "Walk", // glTF animation name
loop: Cesium.ModelAnimationLoop.REPEAT, // NONE | REPEAT | MIRRORED_REPEAT
multiplier: 1.0, // playback speed (must be > 0)
});
anim.start.addEventListener((m, a) => console.log(`Started: ${a.name}`));
// Or play all animations at once
model.activeAnimations.addAll({
loop: Cesium.ModelAnimationLoop.REPEAT,
multiplier: 0.5,
});
});
Additional add options: index, reverse, startTime, stopTime, delay, removeOnStop, animationTime (custom time callback).
Animations require viewer.clock.shouldAnimate = true to advance. A walking character will appear frozen mid-pose (or distorted if at the seam between keyframes) if the clock is stopped.
Animation Events
animation.start.addEventListener((model, animation) => { });
animation.update.addEventListener((model, animation, time) => { });
animation.stop.addEventListener((model, animation) => { });
// Collection-level
model.activeAnimations.animationAdded.addEventListener((model, anim) => { });
model.activeAnimations.remove(animation); // remove one
model.activeAnimations.removeAll(); // remove all
Model Nodes
Override named node transforms for procedural animation (e.g., turret rotation).
model.readyEvent.addEventListener(() => {
const node = model.getNode("Turret");
node.matrix = Cesium.Matrix4.fromScale(
new Cesium.Cartesian3(5.0, 1.0, 1.0), node.matrix
);
});
Properties: name (read-only), id (read-only index), show (boolean), matrix (Matrix4 -- set to undefined to restore original and re-enable glTF animations).
Coloring, Silhouettes, and Feature Picking
// Tint + silhouette
model.color = Cesium.Color.RED.withAlpha(0.5);
model.colorBlendMode = Cesium.ColorBlendMode.MIX;
model.colorBlendAmount = 0.5;
model.silhouetteColor = Cesium.Color.YELLOW;
model.silhouetteSize = 2.0;
Edge Display Mode (Experimental, 1.142+)
For glTF assets using the draft EXT_mesh_primitive_edge_visibility extension,
EdgeDisplayMode controls whether extension-provided edges are hidden, composited
over surfaces, or rendered alone. Models without the extension are unaffected.
import { EdgeDisplayMode, Model } from "cesium";
const model = await Model.fromGltfAsync({
url: "/models/cad-part.glb",
edgeDisplayMode: EdgeDisplayMode.SURFACES_AND_EDGES,
});
viewer.scene.primitives.add(model);
model.edgeDisplayMode = EdgeDisplayMode.EDGES_ONLY; // CAD-style wireframe
model.edgeDisplayMode = EdgeDisplayMode.SURFACES_ONLY; // default
When a glTF has EXT_mesh_features or EXT_structural_metadata, picking returns a ModelFeature:
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((movement) => {
const picked = viewer.scene.pick(movement.endPosition);
if (picked instanceof Cesium.ModelFeature) {
picked.getPropertyIds().forEach((name) => {
console.log(`${name}: ${picked.getProperty(name)}`);
});
picked.color = Cesium.Color.YELLOW;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
Height Reference
// Primitive API -- scene is required for height reference
const model = await Model.fromGltfAsync({
url: "truck.glb",
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
scene: viewer.scene,
});
// Entity API
viewer.entities.add({
position: Cartesian3.fromDegrees(-75.59, 40.03),
model: { uri: "truck.glb", heightReference: Cesium.HeightReference.CLAMP_TO_GROUND },
});
Values: NONE, CLAMP_TO_GROUND, RELATIVE_TO_GROUND, CLAMP_TO_TERRAIN, RELATIVE_TO_TERRAIN, CLAMP_TO_3D_TILE, RELATIVE_TO_3D_TILE.
Particle Systems
ParticleSystem renders billboard-based effects. Position with modelMatrix (world) and emitterModelMatrix (local offset).
Always set viewer.clock.shouldAnimate = true before adding a particle system -- particles only move when the clock is running. A stopped clock produces a static "blob" at the emitter origin rather than a directional plume.
Producing a Legible Vertical Plume
A common failure mode is rendering a diffuse spherical blob instead of a recognizable rising column. To make plumes read as vertical:
- Use
CircleEmitteror a narrowConeEmitterfor upward-biased velocity.SphereEmitterandBoxEmitterradiate in all directions and produce blob-like shapes. - Bias velocity strongly upward -- emitters' local +Z is up in the
modelMatrixframe; setmodelMatrixviaTransforms.eastNorthUpToFixedFrameso +Z is local up. - Use a multi-second
minimumParticleLife/maximumParticleLife(e.g., 1.5-4.0) so particles travel far enough to form a visible column before fading. - Scale particles over their lifetime (
startScalesmall,endScale3-6x larger) so the plume widens with height, matching real smoke. - Keep the emitter footprint smaller than the visible plume. For crater smoke, use a small disk or point source; oversized ground ellipses read as the subject instead of a source marker.
- Avoid solving clipping by making the plume huge. If the top is clipped, first increase camera range or lower particle life/speed/endScale; do not let the plume fill all four edges of the frame.
- Wait for several seconds of simulation time before screenshotting -- the plume needs to develop. Advance the clock or use
viewer.clock.shouldAnimate = trueand wait.
Smoke Trail
import { ParticleSystem, CircleEmitter, Color, Cartesian2, Transforms, Cartesian3 } from "cesium";
viewer.clock.shouldAnimate = true; // required -- particles don't move on a stopped clock
const smokeSystem = new ParticleSystem({
image: "smoke.png",
startColor: Color.LIGHTGRAY.withAlpha(0.7),
endColor: Color.WHITE.withAlpha(0.0),
startScale: 1.0,
endScale: 5.0,
emissionRate: 10,
minimumSpeed: 1.0,
maximumSpeed: 4.0,
minimumParticleLife: 1.2,
maximumParticleLife: 3.0,
imageSize: new Cartesian2(25, 25), // pixel size
emitter: new CircleEmitter(2.0), // radius in meters
modelMatrix: Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(-75.157, 39.978)),
lifetime: 16.0,
loop: true,
});
viewer.scene.primitives.add(smokeSystem);
Volcanic / Crater Smoke Calibration
For terrain-scale smoke such as Mount St. Helens, use an oblique camera and a restrained source marker. The marker should confirm the emitter location without becoming a giant ground disk, and the plume should fit entirely in frame with terrain visible underneath.
import { ParticleSystem, CircleEmitter, Color, Cartesian2, Transforms, Cartesian3 } from "cesium";
viewer.clock.shouldAnimate = true;
const sourcePosition = Cartesian3.fromDegrees(-122.1944, 46.1914, 2549);
viewer.entities.add({
position: sourcePosition,
point: {
pixelSize: 10,
color: Color.ORANGE,
outlineColor: Color.BLACK,
outlineWidth: 2,
},
});
const craterSmoke = new ParticleSystem({
image: createRadialParticle(48, "rgba(180,180,180,0.75)"),
startColor: Color.LIGHTGRAY.withAlpha(0.65),
endColor: Color.WHITE.withAlpha(0.0),
startScale: 0.8,
endScale: 4.0,
emissionRate: 35,
minimumSpeed: 20.0,
maximumSpeed: 45.0,
minimumParticleLife: 2.0,
maximumParticleLife: 4.0,
imageSize: new Cartesian2(22, 22),
emitter: new CircleEmitter(35.0),
modelMatrix: Transforms.eastNorthUpToFixedFrame(sourcePosition),
lifetime: 20.0,
loop: true,
});
viewer.scene.primitives.add(craterSmoke);
viewer.trackedEntity = undefined;
viewer.camera.lookAt(
sourcePosition,
new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(35),
Cesium.Math.toRadians(-22),
6500
)
);
Use this pattern when a prompt asks for smoke over a named geographic source:
- Prefer a
pointor small billboard marker over anellipseunless the prompt specifically asks for a ground footprint. - Keep the plume top, base, and source marker visible at once. A clipped plume or missing marker is a framing failure even if particles are rendering.
- Use an oblique pitch for tall plumes. Near-nadir views flatten the vertical volume into a gray blob.
Emitter Types
import { BoxEmitter, CircleEmitter, ConeEmitter, SphereEmitter } from "cesium";
new BoxEmitter(new Cesium.Cartesian3(10, 10, 10)); // 3D box, velocity outward
new CircleEmitter(2.0); // flat disk, velocity +Z
new ConeEmitter(Cesium.Math.toRadians(30)); // cone tip, velocity toward base
new SphereEmitter(5.0); // sphere, velocity radiates out
Particle Bursts
const firework = new ParticleSystem({
image: getParticleCanvas(),
startColor: Color.RED,
endColor: Color.RED.withAlpha(0.0),
particleLife: 1.0,
speed: 100.0,
imageSize: new Cartesian2(7, 7),
emissionRate: 0, // bursts only
emitter: new SphereEmitter(0.1),
bursts: [
new Cesium.ParticleBurst({ time: 0.0, minimum: 100, maximum: 200 }),
new Cesium.ParticleBurst({ time: 2.0, minimum: 50, maximum: 100 }),
new Cesium.ParticleBurst({ time: 4.0, minimum: 200, maximum: 300 }),
],
lifetime: 6.0,
loop: false,
modelMatrix: Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(-75.597, 40.038)),
});
viewer.scene.primitives.add(firework);
Update Callback (Gravity / Wind)
The updateCallback runs per-particle per-frame for forces like gravity.
const gravityScratch = new Cesium.Cartesian3();
function applyGravity(particle, dt) {
Cesium.Cartesian3.normalize(particle.position, gravityScratch);
Cesium.Cartesian3.multiplyByScalar(gravityScratch, -9.8 * dt, gravityScratch);
particle.velocity = Cesium.Cartesian3.add(particle.velocity, gravityScratch, particle.velocity);
}
const system = new ParticleSystem({
image: "smoke.png",
emissionRate: 20,
emitter: new ConeEmitter(Cesium.Math.toRadians(45)),
updateCallback: applyGravity,
modelMatrix: Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(-105, 40, 1000)),
});
viewer.scene.primitives.add(system);
Framing Particle Effects So the Map Is Visible
Particle effects should stand out against the map underneath, not against the sky or a featureless background. Two common framing failures:
- Camera too close + shallow pitch → the plume fills the frame and the map disappears behind it.
- Camera too steep / near-nadir → tall plumes flatten into a blob and the source geometry reads wrong.
Use viewer.camera.lookAt with HeadingPitchRange to anchor on the emitter and dial in an oblique view that keeps the map context visible:
const position = Cartesian3.fromDegrees(-122.1944, 46.1914, 2549);
viewer.trackedEntity = undefined;
viewer.camera.lookAt(
position,
new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(35), // heading
Cesium.Math.toRadians(-22), // oblique pitch preserves plume height
6500 // increase range until top and base fit
)
);
Guidelines:
- Pitch in the -35° to -60° range for short, ground-hugging effects where map context matters more than vertical extent.
- Pitch in the -10° to -30° range for tall plumes where vertical volume matters (e.g., volcanic columns).
- Verify the marker/source is in-frame by including a billboard or point at the emitter location; if the camera is wrong, the marker will be missing from the screenshot and the issue is obvious.
- Keep background terrain visible around the plume. If particles touch multiple image edges, reduce particle scale/life/speed or increase camera range before taking the screenshot.
- Reset
viewer.trackedEntity = undefinedbeforelookAt, or the tracked entity's reference frame will override your camera transform.
Attaching Particles to a Moving Model
Sync modelMatrix each frame via scene.preUpdate. Use emitterModelMatrix for a local offset (e.g., exhaust pipe).
const entity = viewer.entities.add({
position: sampledPosition,
orientation: new Cesium.VelocityOrientationProperty(sampledPosition),
model: { uri: "truck.glb", minimumPixelSize: 64 },
});
// Local offset to exhaust pipe
const trs = new Cesium.TranslationRotationScale();
trs.translation = new Cesium.Cartesian3(-4.0, 0.0, 1.4);
const emitterModelMatrix = Cesium.Matrix4.fromTranslationRotationScale(trs, new Cesium.Matrix4());
const exhaust = new ParticleSystem({
image: "smoke.png",
startColor: Color.GRAY.withAlpha(0.7),
endColor: Color.TRANSPARENT,
emissionRate: 8,
speed: 2.0,
particleLife: 1.5,
imageSize: new Cartesian2(20, 20),
emitter: new CircleEmitter(0.5),
emitterModelMatrix: emitterModelMatrix,
});
viewer.scene.primitives.add(exhaust);
viewer.scene.preUpdate.addEventListener((scene, time) => {
exhaust.modelMatrix = entity.computeModelMatrix(time, new Cesium.Matrix4());
});
Canvas-Based Particle Images
Generate particle textures dynamically instead of loading image files. A radial gradient produces soft, realistic edges for smoke and water effects.
// Soft radial-gradient particle (smoke, water, fog)
function createRadialParticle(size = 32, colorStop = "rgba(200,200,200,0.9)") {
const c = document.createElement("canvas");
c.width = c.height = size;
const ctx = c.getContext("2d");
const half = size / 2;
const grad = ctx.createRadialGradient(half, half, 0, half, half, half);
grad.addColorStop(0, colorStop);
grad.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, size, size);
return c;
}
// Solid circle (high-contrast, fireworks, sparks)
function createCircleImage(size = 20) {
const c = document.createElement("canvas");
c.width = c.height = size;
const ctx = c.getContext("2d");
ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.fillStyle = "#fff";
ctx.fill();
return c;
}
// Pass canvas directly as image
new ParticleSystem({ image: createRadialParticle(), /* ...other options */ });
Use Color.fromCssColorString for specific particle colors when named colors don't suffice:
startColor: Cesium.Color.fromCssColorString("#66ccff").withAlpha(0.95),
endColor: Cesium.Color.WHITE.withAlpha(0.0),
Entity API Model (ModelGraphics)
For simpler use cases, add a model through the Entity API (see cesiumjs-entities for full coverage).
const entity = viewer.entities.add({
name: "Aircraft",
position: Cartesian3.fromDegrees(-123.074, 44.050, 5000),
orientation: Cesium.Transforms.headingPitchRollQuaternion(
Cartesian3.fromDegrees(-123.074, 44.050, 5000),
new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(135), 0, 0)
),
model: {
uri: "CesiumAir.glb",
minimumPixelSize: 128,
maximumScale: 20000,
silhouetteColor: Color.RED,
silhouetteSize: 2.0,
},
});
viewer.trackedEntity = entity;
Framing trade-off with viewer.trackedEntity: tracking centers the model but uses an auto-computed range derived from the bounding sphere, which often produces a too-close, low-context shot. For prompts that ask for both the model and map context (terrain, landmarks, labels), prefer viewer.camera.flyTo / lookAt to a manually chosen position and clear viewer.trackedEntity = undefined first.
GPM Extension (NGA_gpm_local)
CesiumJS experimentally supports the NGA Geospatial Positioning Metadata glTF extension. Types: AnchorPointDirect, AnchorPointIndirect, CorrelationGroup, GltfGpmLocal, Spdcf. Parsed automatically when loading a glTF with NGA_gpm_local -- the API is experimental and subject to change.
Performance Tips
- Use
.glbover.gltf-- binary format avoids extra HTTP requests and is smaller on the wire. - Enable Draco compression (
KHR_draco_mesh_compression) for 80-90% smaller meshes. - Use KTX2/Basis textures (
KHR_texture_basisu) for GPU-compressed textures; keep dimensions power-of-two. - Set
minimumPixelSizecarefully -- large values force enlargement of distant models, increasing draw cost. - Limit silhouettes -- extra rendering pass per silhouetted model; more than 256 may cause stencil artifacts.
- Reuse scratch
Matrix4objects -- avoid allocating every frame when syncing particle systems to moving entities. - Match emission rate to effect density -- dense jets (fountains, fire) may need rates of 200-1000/s; diffuse smoke works well at 10-60/s. Profile on target hardware.
- Prefer pixel-sized particles (
sizeInMeters: false, default) -- meter-sized particles are expensive at close range. - Set finite
lifetimeon particle systems --Number.MAX_VALUE(default) prevents pool cleanup. - Disable picking for decorations --
allowPicking: falsesaves GPU memory on models that need no interaction. - Destroy when done --
viewer.scene.primitives.remove(model)thenmodel.destroy()to free WebGL resources.
See Also
- cesiumjs-custom-shader -- GLSL authoring for
Model.customShader(struct reference, feature IDs, metadata, vertex displacement) - cesiumjs-materials-shaders -- ImageBasedLighting, post-processing stages for models
- cesiumjs-entities -- Entity API ModelGraphics, data sources, time-dynamic properties
- cesiumjs-3d-tiles -- Cesium3DTileset (uses Model internally), clipping, styling
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/cesiumgs/cesiumjs-skills/cesiumjs-models-particles">View cesiumjs-models-particles on skillZs</a>