mcpbeat

Cesiumjs Models Particles

cesiumgs/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.

5k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
110
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/CesiumGS/cesiumjs-skills --skill cesiumjs-models-particles

What comes with it

3 408 bytes besides the instruction
REFERENCE.md

The instruction itself

24 sections, as written by the author

CesiumJS Models, glTF & Particle Effects

Version baseline: CesiumJS v1.143.

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);

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);

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).

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).

Smoke Trail

import { ParticleSystem, CircleEmitter, Color, Cartesian2, Transforms, Cartesian3 } from "cesium";

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);

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);

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.

function createCircleImage() {
  const c = document.createElement("canvas");
  c.width = c.height = 20;
  const ctx = c.getContext("2d");
  ctx.beginPath();
  ctx.arc(10, 10, 10, 0, Math.PI * 2);
  ctx.fillStyle = "#fff";
  ctx.fill();
  return c;
}

// Pass canvas directly as image
new ParticleSystem({ image: createCircleImage(), /* ...other options */ });

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;

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 .glb over .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 minimumPixelSize carefully -- 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 Matrix4 objects -- avoid allocating every frame when syncing particle systems to moving entities.
  • Keep emission rates low -- each particle is a billboard; rates above 200/s can hurt frame rate. Use bursts for short effects.
  • Prefer pixel-sized particles (sizeInMeters: false, default) -- meter-sized particles are expensive at close range.
  • Set finite lifetime on particle systems -- Number.MAX_VALUE (default) prevents pool cleanup.

10. Disable picking for decorations -- allowPicking: false saves GPU memory on models that need no interaction.

11. Destroy when done -- viewer.scene.primitives.remove(model) then model.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 to use it

Copy the folder

Take cesiumgs/cesiumjs-models-particles from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.