---
name: simengine-modify-scene
description: Modify a SimEngine project made from the first_scene example — add, move or restyle objects in the scene, change the scripted motion (scheduled commands), seeds or expectations in the scenario, or switch to the point_mass behavior variant — then re-run, re-capture and prove the change with digests, expectations and images. Use when the user asks to add an entity or object, move something, animate it, change materials or the camera, or change a SimEngine scenario's behavior.
---

# Modify a scene or behavior

This skill ships with the SimEngine `first_scene` example and drives only
public tools: `simengine_host` (scenario CLI), `simengine_capture` (offscreen render)
and `examples/first_scene/first_scene.py` (the example driver). It assumes a project
created with `simengine-create-project`.

## Compatible versions

Scenario schema `simengine.scenario` **schema_version 1**; capability document 1;
scene state 1; simulation contract 1; module contract 1; scene/asset text formats
`simengine_scene 1` / `simengine_asset 1`; capture manifest 1; example driver 1 and
`simengine.example_project` 1. The driver checks all of them (`incompatible_engine`,
exit 21, on a mismatch). The authoritative field list is
`simengine_host --capabilities` → `capabilities.scenario_schema`; the scene and asset
formats are documented in `fixtures/README.md` of the SimEngine checkout.

## How a project fits together

| File | Owns | Notes |
| --- | --- | --- |
| `scenario.json` | simulation: entities, seed, ticks, scheduled commands, expectations | validated all-or-nothing by `simengine_host --load` |
| `scene.sescene` | presentation: which asset each object uses, material, scale, camera, sun | validated by `simengine_capture --check-scene` |
| `project.json` | which scene and scenarios, viewpoints, capture size | read by the driver only |
| `assets/*.seasset` | meshes and materials (text, meters, +Y up) | each file records its license and tool |

The rule that binds them: **every scenario entity needs a scene instance with exactly the
same name.** When the driver poses the scene, each such instance takes the entity's
position and yaw from the simulation state; instances without an entity (the ground)
stay where they are authored. Generated entities are named `id_prefix` + an index
zero-padded to the width of `count - 1` (`barrel_0` … `barrel_2` for count 3,
`crate_00` … for count 11 or more).

Units and frame: meters, one Cartesian frame, right-handed, `up_axis "+y"` (the scene
convention; the driver refuses any other). Orientations are quaternions `[x, y, z, w]`
that must be pure yaw (rotation about +Y): `[0, sin(a/2), 0, cos(a/2)]`.

Placement reference (asset origins): `crate` is a 0.5 m cube with its origin at the
center of its base (on the ground: y = 0; on another crate: y = 0.5); `spheres`
(mesh `sphere_r0p25`) has its origin at the center (on the ground: y = 0.25; on the
table: y = 1.0); `barrel` (r 0.3 m, h 0.8 m) and `table` (top at y = 0.75 m,
1.2 m × 0.8 m) have their origin on the floor. Sphere materials: `dielectric_rough`,
`dielectric_smooth`, `metal_rough`, `metal_smooth`.

Motion in a core-only build is `simengine.entity.set_position` at a tick (a teleport,
applied at the start of that tick, before systems, in document order). There is no
velocity, interpolation, collision or rotation command. Expectations compare the state
at the end of the run, component-wise, within `tolerance`.

## Variables

```text
SIMENGINE_ROOT=/absolute/path/to/SimEngine
SIMENGINE_BUILD=$SIMENGINE_ROOT/build/linux-release
PROJECT=/absolute/path/to/my_first_scene
```

Prefix each command with them if your shell tool does not keep variables between calls.

## Steps

1. Record the baseline. Run it even if an earlier `run` already passed: step 5 compares
   against `runs/before.json`, which only this command writes (a `check` result has no
   digest or captures and is not a baseline).

```sh
mkdir -p "$PROJECT/runs"
python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD" > "$PROJECT/runs/before.json"
```

2. Make the change in both files. Worked example — add a third crate that starts on the
   ground and is stacked on top of the other two at tick 100:

   - `scenario.json`, append to `entities`:
     `{"id": "crate_c", "position": {"value": [-1.2, 0, 0.9], "unit": "m", "frame": "world"}}`
   - append to `commands`:
     `{"at_tick": 100, "command": "simengine.entity.set_position", "version": 1, "args": {"entity": "crate_c", "position": {"value": [-2, 1, -0.5], "unit": "m", "frame": "world"}}}`
   - append to `expect`:
     `{"name": "crate_c_on_top", "entity": "crate_c", "quantity": "position", "value": {"value": [-2, 1, -0.5], "unit": "m", "frame": "world"}, "tolerance": {"value": 0, "unit": "m"}}`
   - `scene.sescene`, after the other crates:
     `instance crate_c crate * position -1.2 0 0.9`

   Make these edits with your editor. The block below applies exactly the same edit
   (the example's automated test runs it):

```sh
python3 - "$PROJECT" <<'EOF'
import json, pathlib, sys
project = pathlib.Path(sys.argv[1])
scenario_path = project / "scenario.json"
scenario = json.loads(scenario_path.read_text(encoding="utf-8"))
pos = lambda v: {"value": v, "unit": "m", "frame": "world"}
scenario["entities"].append({"id": "crate_c", "position": pos([-1.2, 0, 0.9])})
scenario["commands"].append({"at_tick": 100, "command": "simengine.entity.set_position", "version": 1,
                             "args": {"entity": "crate_c", "position": pos([-2, 1, -0.5])}})
scenario["expect"].append({"name": "crate_c_on_top", "entity": "crate_c", "quantity": "position",
                           "value": pos([-2, 1, -0.5]), "tolerance": {"value": 0, "unit": "m"}})
scenario_path.write_text(json.dumps(scenario, indent=2) + "\n", encoding="utf-8")
scene_path = project / "scene.sescene"
lines = scene_path.read_text(encoding="utf-8").splitlines()
at = max(i for i, line in enumerate(lines) if line.startswith("instance crate_"))
lines.insert(at + 1, "instance crate_c crate * position -1.2 0 0.9")
scene_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
EOF
```

3. Check (no GPU). A mistake shows up here as `scenario_invalid` (23) with a JSON
   pointer per error, or `scene_invalid` (24) with file and line. Fix and repeat.

```sh
python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" check --project "$PROJECT" --build "$SIMENGINE_BUILD"
```

4. Run, capture and verify the changed project.

```sh
python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD" > "$PROJECT/runs/after.json"
python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" verify --project "$PROJECT"
```

5. Prove the change: the state digest changed, the new expectation is evaluated and
   passed, and the images exist. Then open the new `final-front` PNG and look at it.

```sh
python3 - "$PROJECT/runs/before.json" "$PROJECT/runs/after.json" <<'EOF'
import json, sys
before, after = (json.load(open(p, encoding="utf-8")) for p in sys.argv[1:3])
assert before["outcome"] == "PASS" and after["outcome"] == "PASS", (before["failure"], after["failure"])
assert after["final_state_sha256"] != before["final_state_sha256"], "the simulation state did not change"
assert after["expectations"]["failed"] == 0 and after["expectations"]["passed"] == before["expectations"]["passed"] + 1
print("changed:", before["final_state_sha256"][:12], "->", after["final_state_sha256"][:12],
      "| expectations", after["expectations"], "| images", [c["png"] for c in after["captures"]])
EOF
```

A presentation-only change (a material, the camera, `project.json` viewpoints) leaves
`final_state_sha256` unchanged and changes the captured pixels (`pixels_sha256`);
check that instead.

## Behavior with velocity and gravity: the `point_mass` variant

`toss.scenario.json` tosses the ball under gravity with an impulse, using the optional
`point_mass` test module (point kinematics, semi-implicit Euler; no collisions, so a
body falls through the ground if you run long enough). It needs a build that compiles
the module in; a core-only build stops with `module_missing` (22):

```text
cmake --preset linux-release -B "$SIMENGINE_ROOT/build/linux-release-point_mass" -DSIMENGINE_MODULES=point_mass
cmake --build "$SIMENGINE_ROOT/build/linux-release-point_mass"
SIMENGINE_BUILD_POINT_MASS=$SIMENGINE_ROOT/build/linux-release-point_mass
```

```sh point_mass
python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" run --project "$PROJECT" --build "$SIMENGINE_BUILD_POINT_MASS" --scenario toss
python3 "$SIMENGINE_ROOT/examples/first_scene/first_scene.py" verify --project "$PROJECT" --scenario toss
```

Its components and commands (`point_mass.body`, `point_mass.world`,
`point_mass.apply_impulse`, `point_mass.set_velocity`) are listed by `--capabilities`
of that build only. Expected values for its expectations follow the closed form of
semi-implicit Euler: after n ticks of duration dt, v = v0 + n·g·dt and
x = x0 + n·v0·dt + g·dt²·n(n+1)/2 (plus impulse/mass from the tick it is applied).

## On failure

| Exit | `failure.code` | Do this |
| --- | --- | --- |
| 23 | `scenario_invalid` | For each `failure.detail.errors[]`: open the scenario at `line`/`column`, fix the field at `path` (`code` says what: `unit_mismatch`, `unknown_entity`, `unknown_command`, `unknown_component`, `non_normalized_orientation`, …; `expected`/`found` give the values). Nothing ran. |
| 24 | `scene_invalid` | Fix the line in `failure.detail` (`code`, `file`, `line`): e.g. `unknown_mesh`, `syntax_error`, `invalid_scene`. |
| 25 | `pose_mapping_failed` | Add the missing scene instances listed in `failure.detail.entities_without_instance` (same names), or fix a non-yaw orientation / non-`+y` frame. |
| 27 | `expectation_failed` | Compare `failure.detail.failed[].expected` and `.actual`; fix whichever is wrong (the command or the expectation), never widen a tolerance just to pass. |
| 22 | `module_missing` | Use the point_mass build or a core-only scenario. |
| other | see `simengine-run-capture-diagnose` | |

Change only the project's files. Never edit SimEngine sources, the example under
`$SIMENGINE_ROOT/examples/`, or a tolerance to make a run pass.
