OpenPLX Tutorial 03 — Importing and Enhancing a Bulldozer Model from an .agx File
In this tutorial, you will learn how to import a bulldozer model from a .agx file, augment it with tracks and a drivetrain, and set up a terrain for realistic interactions. By the end, you’ll have a complete scene with a functional bulldozer operating on a terrain.

Attached Files
- AGX-file:
bulldozer.agx - Final OpenPLX example: config.openplx, bulldozer.openplx, terrain.openplx, track.openplx, drivetrain.openplx, bulldozer.py
bulldozer.agx is available under data/models/openplx and the other files listed are available under data/openplx/tutorial/t03-bulldozer in your AGX installation.
Before You Begin
Familiarize yourself with the agxViewer keybindings: - 1: Reload the scene to reflect any changes in your code - E: Play/pause the simulation - G: Show/hide collision geometries
For the full keybindings, visit the AGX Keybindings Documentation.
Model Structure
In this tutorial, we are going to augment an agx model of a bulldozer with definitions for a shovel and two rippers. We are also going to define reusable bundles for a track, drivetrain and terrain and combine them in a bulldozer simulation. Finally, we are going to set up signals through a python script to control the machine.

Step 1: Import the Bulldozer Model
The .agx file bulldozer.agx which is available under data/models/openplx in you AGX installation, contains a basic bulldozer model with rigid bodies, constraints, and visuals that have been defined based on a CAD model using Momentum. For now, it lacks tracks, a drivetrain, and terrain. These will be defined using OpenPLX.
Start by creating a new file called bulldozer.openplx with the following content:
import @"bulldozer.agx" as AgxBulldozer
Scene is Physics3D.System:
bulldozer is AgxBulldozer
Load the file with the simulation paused to view the imported bulldozer:
agxViewer bulldozer.openplx -p

Press G in the viewer to see the collision geometries defined in Momentum:

Step 2: Add a Terrain
To prevent the bulldozer from falling indefinitely, add a Terrain.
Add an offset to the bulldozer position to avoid intersection with the terrain.
import @"bulldozer.agx" as AgxBulldozer
Scene is Physics3D.System:
bulldozer is AgxBulldozer:
local_transform.position.z: 1.8
terrain is Terrain.Terrain:
num_elements_x: 300
num_elements_y: 300
element_size: 0.2
max_depth: 5.0
Press
1in the viewer to reload the scene with the added terrain. PressEto start or pause the simulation.
The bulldozer does not have controls yet. For now, use hold ctrl and drag with the mouse to interact with the model:

Step 3: Split Model into Multiple Files
To organize the setup, create an OpenPLX bundle. Add a config.openplx file in the same directory as bulldozer.openplx with the following content:
config is BundleConfig:
name: "bulldozer"
dependencies: ["Vehicles", "Terrain", "Visuals", "Robotics"]
Here you can read more about what an OpenPLX bundle is and how to structure the bundle content and how to depend on other bundles.
Create a separate file, terrain.openplx. Add the Terrain model to the new file:
BulldozerTerrain is Terrain.Terrain:
num_elements_x: 300
num_elements_y: 300
element_size: 0.2
max_depth: 5.0
Update bulldozer.openplx to reference the extracted terrain. Extract the bulldozer definition into a separate model inside the bulldozer.openplx file. This will give us a cleaner structure to work with when adding new components to the model.
import @"bulldozer.agx" as AgxBulldozer
Bulldozer is AgxBulldozer
Scene is Physics3D.System:
bulldozer is Bulldozer:
local_transform.position.z: 1.8
terrain is BulldozerTerrain
Reload the scene to ensure that everything looks the same as in the last step.
Step 4: Add Tracks
Next, we will define a Track model that can be reused for both the left and right track. For this bulldozer model, we want the track to be generated around one Sprocket and two Rollers that are already present in the .agx model.
To improve simulation performance, the tracks will need some AGX specific parameters, specifically we want to enable merging. This merges the track nodes into segments and reduces the number of contact calculations in the system.
Create a new file called track.openplx and define the Track:
BulldozerLinkDescription is Vehicles.Tracks.BoxLinkDescription:
width: 0.61
height: 0.093
BulldozerTrackBelt is Vehicles.Tracks.FixedLinkCountBelt:
link_count: 22
link_description becomes BulldozerLinkDescription
CenterAxis is Math.Vec3: -Math.Vec3.Z_AXIS()
# Track system to generate the belt around the provided road wheels.
BulldozerTrack is Vehicles.Tracks.System:
.agx_set_enable_merge: true # .agx specific merge settings
belt becomes BulldozerTrackBelt
belt.link_count: 60
initial_distance_tension: 1e-3
sprocket is Vehicles.Tracks.CylindricalSprocket:
local_center_axis: CenterAxis
roller_1 is Vehicles.Tracks.CylindricalIdler:
local_center_axis: CenterAxis
roller_2 is Vehicles.Tracks.CylindricalIdler:
local_center_axis: CenterAxis
road_wheels: [sprocket, roller_1, roller_2]
Next, add the tracks to bulldozer.openplx. Knowing the names of the structural components defined in the .agx file is key to make these connections. When working with your own models, you can find the names of .agx bodies and constraints either by referencing to the original Momentum project, or by using the anytoopenplx which converts the .agx file to openplx.
python -m agxPythonModules.openplx.anytoopenplx bulldozer.agx > openplx_file.openplx
Note! If you convert .agx files to .openplx files make sure to not put the generated .openplx files in the same bundle that imports the .agx files since the .openplx files will automatically be imported and conflict with the imported .agx files.
For the bulldozer, define the tracks using the sprockets and rollers from the AGX model:
import @"bulldozer.agx" as AgxBulldozer
Bulldozer is AgxBulldozer:
left_track is BulldozerTrack:
sprocket:
body: LeftSprocket
radius: LeftSprocket.LeftSprocketCollisionGeometry_1.radius
width: LeftSprocket.LeftSprocketCollisionGeometry_1.height
roller_1:
body: LeftRearRollerWheel
radius: LeftRearRollerWheel.LeftRearRollerWheelCollisionGeometry_1.radius
width: LeftRearRollerWheel.LeftRearRollerWheelCollisionGeometry_1.height
roller_2:
body: LeftFrontRollerWheel
radius: LeftFrontRollerWheel.LeftFrontRollerWheelCollisionGeometry_1.radius
width: LeftFrontRollerWheel.LeftFrontRollerWheelCollisionGeometry_1.height
right_track is BulldozerTrack:
sprocket:
body: RightSprocket
radius: RightSprocket.RightSprocketCollisionGeometry_1.radius
width: RightSprocket.RightSprocketCollisionGeometry_1.height
roller_1:
body: RightRearRollerWheel
radius: RightRearRollerWheel.RightRearRollerWheelCollisionGeometry_1.radius
width: RightRearRollerWheel.RightRearRollerWheelCollisionGeometry_1.height
roller_2:
body: RightFrontRollerWheel
radius: RightFrontRollerWheel.RightFrontRollerWheelCollisionGeometry_1.radius
width: RightFrontRollerWheel.RightFrontRollerWheelCollisionGeometry_1.height
Scene is Physics3D.System:
bulldozer is Bulldozer:
local_transform.position.z: 1.8
terrain is BulldozerTerrain
Reload the simulation to see the newly attached tracks:

Press G to verify that the relevant sprockets and rollers are highlighted in new colors. This confirms they are now part of the Track system:

Step 5: Add shovel and ripper model
The .agx model of the bulldozer has a shovel and rippers attached. To make the tools interact with the terrain, we need to configure them using OpenPLX Terrain.Shovel.
To define a shovel we need to configure the top_edge and cutting_edge. We can calculate these using reference positions and vectors imported from the .agx file. The final result will look like this:
import @"bulldozer.agx" as AgxBulldozer
Bulldozer is AgxBulldozer:
left_track is BulldozerTrack:
# ... track settings left out for brevity
right_track is BulldozerTrack:
# ... track settings left out for brevity
shovel is Terrain.Shovel:
body: Shovel
top_edge: Math.Line.from_points(Shovel.ShovelUpperRight.position, Shovel.ShovelUpperLeft.position)
cutting_edge: Math.Line.from_points(Shovel.ShovelLowerRight.position, Shovel.ShovelLowerLeft.position)
left_ripper is Terrain.Shovel:
body: LeftRipper
top_edge: Math.Line.from_points(LeftRipper.LeftRipperUpperRight.position, LeftRipper.LeftRipperUpperLeft.position)
cutting_edge: Math.Line.from_points(LeftRipper.LeftRipperLowerRight.position, LeftRipper.LeftRipperLowerLeft.position)
right_ripper is Terrain.Shovel:
body: RightRipper
top_edge: Math.Line.from_points(RightRipper.RightRipperUpperRight.position, RightRipper.RightRipperUpperLeft.position)
cutting_edge: Math.Line.from_points(RightRipper.RightRipperLowerRight.position, RightRipper.RightRipperLowerLeft.position)
Scene is Physics3D.System:
bulldozer is Bulldozer:
local_transform.position.z: 1.8
terrain is BulldozerTerrain
Reload the scene and confirm that the shovel and rippers have new bounding boxes and shovel visuals attached to them:

Hold ctrl and drag with the mouse to verify that the shovel models are interacting with the terrain:

Step 6: Add a Drivetrain
Create a new file called drivetrain.openplx. Here, we will define a drivetrain model that will later be used to steer the bulldozer:
BulldozerDriveTrain is Physics3D.System:
engine_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
central_gear_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
left_clutch_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
right_clutch_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
diff_left_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
diff_right_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
left_brake_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
right_brake_shaft is DriveTrain.Shaft:
inertia.inertia: 0.1
gear_box is DriveTrain.GearBox:
forward_gears: [80, 45, 27]
reverse_gears: [80, 45, 27]
connectors: [engine_shaft.output, central_gear_shaft.input]
initial_gear: 1
differential is DriveTrain.Differential:
gear_ratio: 2.0
drive_shaft: central_gear_shaft
left_axle_shaft: diff_left_shaft
right_axle_shaft: diff_right_shaft
locked_up: true
locked_up_output.enabled: true
left_clutch is DriveTrain.AutomaticClutch:
initial_engagement_fraction: 0.0
initially_engaged: true
torque_capacity: 40000000
min_relative_slip_ratio: 0.0001
connectors: [differential.left_axle_shaft.output, left_clutch_shaft.input]
right_clutch is DriveTrain.AutomaticClutch:
initial_engagement_fraction: 0.0
initially_engaged: true
torque_capacity: 40000000
min_relative_slip_ratio: 0.0001
connectors: [differential.right_axle_shaft.output, right_clutch_shaft.input]
left_brake is DriveTrain.ManualBrake:
initial_engagement_fraction: 0.0
torque_capacity: 6000000
min_relative_slip_ratio: 0.0001
connectors: [left_clutch_shaft.output, left_brake_shaft.input]
right_brake is DriveTrain.ManualBrake:
initial_engagement_fraction: 0.0
torque_capacity: 6000000
min_relative_slip_ratio: 0.0001
connectors: [right_clutch_shaft.output, right_brake_shaft.input]
motor is Physics1D.Interactions.RotationalVelocityMotor:
target_speed: 0.0
connectors: [engine_shaft.input]
left_hinge_actuator is DriveTrain.HingeActuator:
connector_1d: left_brake_shaft.output
right_hinge_actuator is DriveTrain.HingeActuator:
connector_1d: right_brake_shaft.output
Attach the drivetrain in bulldozer.openplx. Similar to defining the tracks, the LeftSprocketHinge and RightSprocketHinge are defined in the AGX model:
Bulldozer is AgxBulldozer:
#...Previous Tracks and Shovel definitions...
drive_train is BulldozerDriveTrain:
left_hinge_actuator.mate_3d: LeftSprocketHinge
right_hinge_actuator.mate_3d: RightSprocketHinge
Scene is Physics3D.System:
bulldozer is Bulldozer:
local_transform.position.z: 1.8
terrain is BulldozerTerrain
Step 7: Disable collisions between tracks and chassis
To prevent the push bar from colliding with the tracks we will disable collisions between the track systems and the push bars:
Bulldozer is AgxBulldozer:
# ... other attributes left out for brevity
tracks_collision_group is Simulation.CollisionGroup:
systems: [left_track, right_track]
chassis_collision_group is Simulation.CollisionGroup:
bodies: [Push_Bar, Push_Bar2]
tracks_chassis_collision_pair is Simulation.DisableCollisionPair:
group1: tracks_collision_group
group2: chassis_collision_group
Step 8: Add Controls
It is time to add steering to the bulldozer.
The the Shovel and Rippers are maneuvered using different Linear Velocity Motor instances defined in the AGX model. They are controlled using motor.target_linear_velocity_input signals.
The drivetrain is maneuvered using the RotationalVelocityMotor and ManualBrakes defined in drivetrain.openplx. It is is controlled
through the motor.target_angular_velocity_input and *_brake.engagement_fraction_input signals.
The attached script bulldozer.py will load the bulldozer.openplx file and bind keyboard inputs to actuate the drivetrain and manipulate the bulldozer's movements.
Excerpt for retreiving the signals:
# Retrieve the input objects from the scene
# Drivetrain
assert(motor_input := openplx_scene.getObject("bulldozer.drive_train.motor.target_angular_velocity_input"))
clutch_input = {
Position.LEFT: openplx_scene.getObject("bulldozer.drive_train.left_clutch.engagement_fraction_input"),
Position.RIGHT: openplx_scene.getObject("bulldozer.drive_train.right_clutch.engagement_fraction_input")
}
assert clutch_input[Position.LEFT]
assert clutch_input[Position.RIGHT]
brake_input = {
Position.LEFT: openplx_scene.getObject("bulldozer.drive_train.left_brake.engagement_fraction_input"),
Position.RIGHT: openplx_scene.getObject("bulldozer.drive_train.right_brake.engagement_fraction_input" )
}
assert brake_input[Position.LEFT]
assert brake_input[Position.RIGHT]
# Shovel
assert(shovel_left_lift_input := openplx_scene.getObject("bulldozer.LeftLiftPrismatic_motor.target_linear_velocity_input"))
assert(shovel_right_lift_input := openplx_scene.getObject("bulldozer.RightLiftPrismatic_motor.target_linear_velocity_input"))
assert(shovel_left_tilt_input := openplx_scene.getObject("bulldozer.LeftTiltPrismatic_motor.target_linear_velocity_input"))
assert(shovel_right_tilt_input := openplx_scene.getObject("bulldozer.RightTiltPrismatic_motor.target_linear_velocity_input"))
# Rippers
assert(ripper_left_lift_input := openplx_scene.getObject("bulldozer.LeftRipperLiftPrismatic_motor.target_linear_velocity_input"))
assert(ripper_right_lift_input := openplx_scene.getObject("bulldozer.RightRipperLiftPrismatic_motor.target_linear_velocity_input"))
Add the python script to the same directory as the OpenPLX files and reload the model using:
python bulldozer.py
You should now have a model that can be controlled using your keyboard. Keybindings:
- UP / DOWN Arrows: Use the drivetrain to drive forward and backward.
- INSERT / DELETE: Engage the brake on the left and right track.
- PG UP / PG DOWN: Move the shovel up and down.
- HOME / END: Tilt the shovel forward and backward.
- LEFT / RIGHT Arrows: Move the rippers up and down.

Step 9: Define surface contact model between tracks and terrain
You will notice that the tracks keep sliding over the terrain. To get a better grip we will add a Surface Contact Model between the tracks and the terrain.
Add a standard Terrain Material from the Material Library to the terrain in terrain.openplx:
BulldozerTerrain is Terrain.Terrain:
num_elements_x: 300
num_elements_y: 300
element_size: 0.2
max_depth: 5.0
material: Terrain.MaterialLibrary.dirt_1
Create another Material for the tracks. Add it to the link descriptor in track.openplx:
const BulldozerTrackMaterial is Physics.Geometries.Material:
density: 1800
BulldozerLinkDescription is Vehicles.Tracks.BoxLinkDescription:
width: 0.305*2
height: 0.0465*2
contact_geometry.material: BulldozerTrackMaterial
Define the Surface Contact Model in bulldozer.openplx:
import @"bulldozer.agx" as AgxBulldozer
TrackTerrainContactModel is Physics.Interactions.SurfaceContact.Model:
material_1: BulldozerTrackMaterial
material_2: Terrain.MaterialLibrary.dirt_1
tangential_restitution: 0.0
normal_restitution: 0.0
friction becomes Physics.Interactions.Dissipation.DryConstantNormalForceFriction:
normal_force: 10000
coefficient: 1.0
Bulldozer is AgxBulldozer:
#...Bulldozer definition...
Scene is Physics3D.System:
bulldozer is Bulldozer:
local_transform.position.z: 1.8
terrain is BulldozerTerrain
Finally, add the Surface Contact Model to the Physics3D.System in bulldozer.openplx:
Scene is Physics3D.System:
bulldozer is Bulldozer:
local_transform.position.z: 1.8
terrain is BulldozerTerrain
# Surface Contact Model
track_terrain_contact_model is TrackTerrainContactModel
This completes the setup of a basic bulldozer model. You are now ready to get started on your own machines!