Group 11 — ME 635 Modeling and Simulation, Spring 2026
This folder contains a hybrid 3D companion to the 2D MATLAB simulation
Multirobot_Warehouse_Dynamic_2D.m. The 2D logic runs unchanged; its
robot trajectories are captured and replayed in a Simscape Multibody
world that visually mirrors the warehouse layout.
| File | Stage | Purpose |
|---|---|---|
ME635_3D_Pose_Logger.m |
1 | Wraps the 2D simulation; logs poses + wheel angles + payload events to robot_trajectories.mat. |
build_3D_warehouse_model.m |
2 | Programmatically builds ME635_3D_Warehouse.slx. |
run_ME635_3D.m |
3 | One-button entry point: runs the logger, builds/loads the model, runs the simulation. |
robot_trajectories.mat |
(output of Stage 1) | Per-robot timeseries + map metadata. |
ME635_3D_Warehouse.slx |
(output of Stage 2) | The Simulink model. |
Required toolboxes (MATLAB R2025a or later recommended):
- MATLAB
- Simulink
- Simscape
- Simscape Multibody
- Stateflow
- Robotics System Toolbox
- Navigation Toolbox
Optional (you said you have these — fine):
- Robotics Playground (FX 67157)
- Robotics Playground Expansion Pack (FX 80080)
The build script does not depend on the Playground toolboxes — it constructs robot bodies from Simscape Multibody primitives styled to look like TurtleBot3 Waffle Pi (1.5 m × 1.0 m × 0.4 m chassis, two side drive wheels, two casters, top "lidar" cap). If you later want to swap in the actual TurtleBot3 mesh from the Expansion Pack, see Section 6.
In MATLAB, set the current folder to this directory and run:
run_ME635_3DThat's it. You'll see:
- The 2D simulation figure animating (with logging hooks running invisibly).
- After it finishes,
robot_trajectories.matis saved here. - The Simulink model is built and opened.
- The simulation runs and Mechanics Explorer pops up with the 3D view.
You can also run each stage separately:
ME635_3D_Pose_Logger % Stage 1 only
build_3D_warehouse_model % Stage 2 only — needs .mat from Stage 1
sim('ME635_3D_Warehouse') % Stage 3 only — needs .slx from Stage 2robot_trajectories.mat holds:
tsRobots 1×N cell, each is timeseries [x, y, theta] — body pose
wheelTs 1×N cell, each is timeseries [phi_L, phi_R] — wheel angles (rad)
payTs 1×N cell, each is timeseries scalar 0 or 1 — payload visible flag
NUM_R scalar, number of robots
wheelR 0.15 (m)
trackW 1.0 (m)
BODY_LEN 1.5 (m)
BODY_WID 1.0 (m)
BODY_HGT 0.4 (m)
mapMeta struct with shelf, station, barrier coordinates
robotColors N×3 RGB matching the 2D neon palette
The build script reads this and pushes per-robot variables (pose_R1,
wheels_R1, payload_R1, etc.) to the base workspace so that
From Workspace blocks in the model can reference them by name.
Top-level (one-time foundation):
World Frame ─── Mechanism Configuration ─── Solver Configuration
│
├──── Static World (Floor, 12 Shelves, Barrier, 5 Station markers)
│
├──── Robot R1 (Subsystem)
├──── Robot R2 (Subsystem)
├──── Robot R3 (Subsystem)
└──── Robot R4 (Subsystem)
Each robot subsystem:
PMC inport ─── Planar Joint (Px, Py, Rz) ─── Rigid Transform ─── Body (Brick Solid)
▲ ▲ ▲ │
q_x q_y q_th (from FW_pose_R<n> via Demux) ├── Drive wheel L (Revolute Joint, motion in)
├── Drive wheel R (Revolute Joint, motion in)
├── Caster F (rigid sphere)
├── Caster B (rigid sphere)
├── Lidar cap (rigid cylinder, robot color)
└── Prismatic-Z ─── Payload (Brick Solid)
External signal flow into each subsystem:
FW_pose_R<n> ─── Demux (3) ──► q_x, q_y, q_th
FW_wheels_R<n> ─── Demux (2) ──► q_phiL, q_phiR
FW_payload_R<n> ─── Gain(5.4) ─── Bias(-5) ──► q_payZ
The Gain+Bias maps the 0/1 payload flag to z-position:
p=1 → z=0.4 m (visible), p=0 → z=-5 m (hidden underground).
The build script is wrapped in extensive try/catch. When something
fails, you'll see warnings like:
[BUILD warn] set_param Body.GeometryLengthCartesian = [1.5 1 0.4] failed: Invalid parameter
That tells you exactly which block parameter to fix. Below are the most likely culprits.
If the very first add_block call fails (World Frame), your library
path differs from sm_lib/.... Check with:
sm_lib % opens the library browser at the right placeIf MATLAB tells you sm_lib doesn't exist, your install is older or
named differently. Try mech_lib (legacy name pre-R2019a). Find/replace
sm_lib → <your library> in build_3D_warehouse_model.m.
Block parameter names changed at various releases. The script uses these (R2024+ convention):
| Block | Parameter | Value |
|---|---|---|
| Brick Solid | GeometryLengthCartesian |
[lx ly lz] |
| Cylindrical Solid | GeometryRadius, GeometryLength |
scalars |
| Solid (any) | ColorVector |
[r g b] |
If set_param warns on these, open the block in the GUI, look at the
mask parameter names, and update the script. Right-click → Mask →
Edit Mask shows internal names. Or use:
get_param('ME635_3D_Warehouse/Floor', 'ObjectParameters')…to list valid parameter names for any specific block.
If wheels/casters/lidar/payload all stack at the body's center instead of branching out to their correct positions, the issue is the offset between the body and each child — not a missing reference frame on the brick.
R2025b note: the Edit Frame dialog on a Solid block does NOT have a numeric offset (XYZ translation) field anymore. The only options are "At Reference Frame Origin" and "Based on Geometric Feature". Older guides describing a Translation
[x y z]text input are obsolete.
The fix is to put each offset into the Rigid Transform block already
in the chain, not to bake new frames into the brick. The build script
inserts RT_WL, RT_WR, RT_CF, RT_CB, RT_lidar, RT_PrismPay
between the body and each child precisely for this purpose. See §11.3
for the offset values to enter.
This is the single most likely manual-fix step.
The script expects the joint to expose ports named q_Px, q_Py,
q_Rz. If your release names them differently (e.g. Px, Py, Rz
or numbered ports 1, 2, 3), the add_line calls will warn.
Fix: open R<n>/PlanarJ block, look at the actual port labels, and
search/replace q_Px, q_Py, q_Rz, q_Pz accordingly in the script.
If the simulation errors with Variable 'pose_R1' not found in base workspace:
- Re-run
ME635_3D_Pose_Logger.m— it must complete and callassignin('base', ...)for these variables. - Or load manually:
S = load('robot_trajectories.mat'); for ri = 1:S.NUM_R assignin('base', sprintf('pose_R%d', ri), S.tsRobots{ri}); assignin('base', sprintf('wheels_R%d', ri), S.wheelTs{ri}); assignin('base', sprintf('payload_R%d', ri), S.payTs{ri}); end
Driving a joint's position from a From Workspace timeseries means
Simscape Multibody must compute velocity and acceleration internally.
For dt = 0.2 s data this can cause solver chatter.
Fix (recommended): in each Planar Joint and Revolute Joint block, set:
- Position Targets → Specify → "Provided by Input"
- Velocity Targets → Specify → "Calculated by Differentiation"
- Acceleration Targets → Specify → "Calculated by Differentiation"
If you still see jitter, increase the timeseries sample rate by linear interpolation in the logger:
% In ME635_3D_Pose_Logger.m, just before save():
fineT = (0:0.02:t(end))'; % 50 Hz
tsRobots{ri_} = resample(tsRobots{ri_}, fineT);
wheelTs{ri_} = resample(wheelTs{ri_}, fineT);
payTs{ri_} = resample(payTs{ri_}, fineT);After the model opens, the camera defaults to a generic view. To get the isometric front-right preset:
- Open Mechanics Explorer (it opens automatically on first sim run).
- Right-click the 3D viewport → View Convention → "Y up".
- Left-click + drag to rotate to your preferred angle.
- Mechanics Explorer → File → Save Working State → save as
ME635_3D_Warehouse_view.mat. - The next time you open the model, Mechanics Explorer remembers it.
If you want the actual TurtleBot3 Waffle Pi from
Robotics Playground Expansion Pack:
- In the Library Browser, find the TurtleBot3 Waffle Pi mechanism
block (path varies by release; typically
Robotics Playground Expansion Pack / Robots / TurtleBot3 / Waffle Pi). - Drag a copy into each
R<n>subsystem, replacing the primitiveBodyBrick Solid + wheels. - Connect the TurtleBot3 block's frame port to your Planar Joint's follower frame.
- The TurtleBot3 block has its own internal wheel actuators — you'll
need to either disable them (set wheel command inputs to 0) or, if
you want the TurtleBot3's own wheels to spin from your wheel
timeseries, route
phi_L/phi_Rto its wheel-velocity inputs after differentiating:omega_L = du/dt(phi_L).
The primitive build is recommended for the report demo because it gives you full control over chassis colour matching to the 2D palette (R1 yellow, R2 cyan, R3 magenta, R4 orange). The Expansion Pack TurtleBot3 has a baked-in white/grey shell.
After running the simulation, record Mechanics Explorer:
smwritevideo('ME635_3D_Warehouse', 'ME635_3D_Warehouse.mp4', ...
'FrameRate', 30, 'PlaybackSpeedRatio', 1);If smwritevideo doesn't exist in your install, use the Mechanics
Explorer GUI: Tools → Record video.
The pose logger (Stage 1) is the bulletproof part — it just runs the 2D
sim with a few extra assignin calls. If robot_trajectories.mat is
not produced, that's the only file you need to fix.
The build script (Stage 2) is the fragile part. If it cannot produce a
working .slx, you can build the model manually:
- Create a blank Simulink model.
- Drop in:
World Frame,Mechanism Configuration,Solver Configuration. Connect them. - Drop one Brick Solid (50×50×0.05) connected via Rigid Transform to
World, with translation
[25 25 -0.025]. That's your floor. - For each shelf in
mapMeta.shelfOrigins: drop a Rigid Transform + Brick Solid, with translation =[sx + 1, sy + 2.5, 1.5]and geometry =[2, 5, 3]. - For each robot: build a Subsystem as described in Section 5.
- Add From Workspace blocks reading
pose_R<n>,wheels_R<n>,payload_R<n>.
A reasonable minimum is one robot driven by pose_R1 — once that
works, copy/paste it 3 more times and rename.
- 4 robots × ~120 sample/s for a typical 60–120 s run = a few thousand samples per robot. Negligible for the .mat file.
- The 12 shelves × 4 robots × ~10 blocks each = ~150 blocks total in the model. Compiles in well under a minute on a typical laptop.
- Mechanics Explorer rendering of 50×50 m world at 1920×1080 runs at ~30 FPS on integrated graphics; smoother with discrete GPU.
The build script gets you ~80% of the way there. The remaining ~20% is joint motion-input enabling and reference-frame addition that has to be done in the Simulink GUI because the underlying mask logic only exposes extra ports/frames after a checkbox toggle. This section is a strict step-by-step recipe — follow it once for R1, then copy the pattern to R2, R3, R4.
The build script's last console block (Diagnostic — DialogParameters available on key blocks) tells you the exact mask parameter names your
release uses — keep that output handy.
Open ME635_3D_Warehouse.slx. At top-left you should see:
World(World Frame)MechCfg(Mechanism Configuration)SolverCfg(Solver Configuration)
These three need to share one physical-modeling network. If your release auto-connected them, fine. If they're floating, draw three lines:
- From
Worldright port toMechCfgleft port. - Branch from that line over to
SolverCfgleft port.
You can also delete MechCfg and SolverCfg and re-drag fresh ones from
the library — Simulink usually auto-attaches them when dropped near
World.
The 12 shelves + barrier + 5 station markers should be present along the top of the diagram. If any shelf has wrong dimensions (e.g. it's a default 1×1×1 cube instead of 2×5×3 m):
- Double-click the offending
Shelf_<n>block. - Geometry tab → set the dimensions field (likely named
Length,LengthCartesian, orSidein your release) to[2 5 3]. - Inertia tab → set Density (or rely on default ~1000 kg/m³).
- Graphics → Visual Properties → Color → set to the dark blue
[0.12 0.26 0.50].
Same drill for Floor ([50 50 0.05]), Barrier ([4.4 1.2 0.6]),
and ShelfC4_<n> ([1 5 3]).
For the station cylinder markers (Stn_Charging, etc.) — verify radius
1.5 m, height 0.05 m.
Each robot's Body has six children that must branch off it at specific offsets: WL, WR, CF, CB, Lidar, PrismPay. The build script already inserts a Rigid Transform block in each chain — that's where the offset belongs in modern Simscape Multibody.
Why we don't add frames to the brick in R2025b: the "Edit Frame" dialog on a Brick Solid only offers "At Reference Frame Origin" and "Based on Geometric Feature". There is no numeric offset field anymore. So we leave the brick with its default
Rframe and put all offsets in the existing Rigid Transforms.
For each robot R1…R4, open each Rigid Transform inside the subsystem and set its translation:
| Block | Translation (m) | Meaning |
|---|---|---|
RT_WL |
[0, 0.55, 0] |
Left wheel mount |
RT_WR |
[0, -0.55, 0] |
Right wheel mount |
RT_CF |
[ 0.55, 0, -0.20] |
Front caster |
RT_CB |
[-0.55, 0, -0.20] |
Back caster |
RT_lidar |
[0, 0, 0.20] |
Lidar cap on top |
RT_PrismPay |
[0, 0, 0.20] |
Payload anchor on top |
In each Rigid Transform dialog:
- Double-click the block.
- Translation → Method =
Cartesian. - Translation → Offset = the vector from the table above (paste
it as
[0 0.55 0]etc., space-separated, unitsm). - Rotation =
None(default). - OK / Apply.
The connection chain stays exactly as the script built it:
Body/R → RT_<child>/B → ... → child block. No need to touch the
brick's frame editor.
Tip: do this for R1 first, then right-click the entire R1 subsystem → Copy → Paste to replace R2/R3/R4 with renamed copies. The Rigid Transform offsets carry through the copy.
This is what exposes the px, py, qz, q, and pz signal-input
triangles on the joints that the recorded poses drive.
Where the option lives in R2025b: the "Provided by Input" setting is in the Actuation → Motion dropdown, NOT in the State Targets section. State Targets only seeds the solver's initial guess at t=0 and does not drive the joint over time. Older guides describing a "Provided by Input" checkbox inside State Targets are obsolete — ignore that section (or leave it unchecked).
For each Planar Joint (R1/PlanarJ, R2/PlanarJ, R3/PlanarJ,
R4/PlanarJ):
- Double-click the Planar Joint block.
- Expand X Prismatic Primitive (Px) in the parameter tree.
- Scroll to the Actuation group → click the Motion dropdown (default value: "Automatically Computed") → select "Provided by Input".
- Repeat for Y Prismatic Primitive (Py): Actuation → Motion → "Provided by Input".
- Repeat for Z Revolute Primitive (Rz): Actuation → Motion → "Provided by Input".
- Click OK / Apply. The block now shows three signal-input triangles
on the left side, labelled
px,py,qz(one per primitive). - Wire the subsystem inports to those ports:
q_x→ PlanarJpxq_y→ PlanarJpyq_z→ PlanarJqz(this is the body rotation about Z)
For each Revolute Joint at the wheels (RJ_WL, RJ_WR × 4 robots):
- Double-click.
- Z Revolute Primitive (Rz) → Actuation → Motion → "Provided by Input".
- OK. The joint now has a
qsignal input port. Wireq_phiL→ RJ_WLq;q_phiR→ RJ_WRq.
For each Prismatic Joint at the payload (PrismPay × 4 robots):
- Double-click.
- Z Prismatic Primitive (Pz) → Actuation → Motion → "Provided by Input".
- OK. The joint now has a
pzsignal input port. Wireq_payZ→ PrismPaypz.
If you drag from a subsystem inport (e.g. q_x) toward the joint's
input port and Simulink leaves a dashed red line that won't commit:
- Click the broken red segment, hit Delete — don't try to fix it by drawing more.
- Drag from the source's output edge directly to the destination triangle. Release the mouse exactly on the triangle (Simulink highlights it green when you're hovered on it). Releasing on empty space — even close to it — leaves a stub.
- If the autorouter keeps colliding with
RT_CBor other blocks between the inports and the joint, hold Shift while dragging to force a straight segment, or temporarily move the obstructing block aside, draw the line, then move it back. - Verify you clicked OK / Apply on the joint dialog before drawing. The triangle ports don't accept connections until the Motion dropdown change is committed.
In R2025b, when you set Actuation → Motion = "Provided by Input" with the default sub-options, the joint takes position only and the solver computes velocity and acceleration by differentiation under the hood. You don't need to do anything in this section in R2025b — this step is essentially a no-op.
If you ever see an "Underspecified motion at primitive Px" warning at simulation time, open the joint, look near the Motion dropdown for an "Input Handling" sub-option, and confirm it's set to "Position only" (or "Provide position; compute velocity and acceleration"). Don't re-touch State Targets — that section is unrelated to motion drive.
- Click the green ▶ Run button (or
sim('ME635_3D_Warehouse')in the command window). - Mechanics Explorer pops up.
- Robots should immediately appear at their start poses (corners of the warehouse) and begin moving.
If the model errors with "Cannot resolve variable 'pose_R1' in base
workspace": re-run ME635_3D_Pose_Logger.m (which calls assignin
to push the pose timeseries to the base workspace) before running the
3D model.
In Mechanics Explorer:
- View menu → Convention → "Y up".
- Drag the 3D viewport with the mouse to rotate to a front-right isometric view.
- File → Save Working State → name it
ME635_3D_Warehouse_view.mat. Loading the model next time restores the camera.
- Foundation verification (§11.1–§11.2): ~5 minutes.
- One-robot reference frames + joint enables (§11.3–§11.5 for R1): ~15 minutes.
- Copy-paste R2, R3, R4 + verify wheel offsets: ~10 minutes.
- Camera + run + first error round-trip: ~10 minutes.
Realistic total: 30–45 minutes once you have the script-built model open. The bulk of the model topology — 50×50 floor, 12 shelves, 4 robots' chassis + wheels + casters + lidar + payload boxes, all the From Workspace + Demux signal wiring — that's done by the script.
Easiest fix: delete the broken subsystem entirely, copy a working one,
rename, and update its FW_pose_R* / FW_wheels_R* / FW_payload_R*
VariableName parameters to match the robot index. The rest of the
top-level signal wiring is identical between robots.