Not on Windows? In your AGX Dynamics installation, navigate to data/openplx/tutorials/t03-bulldozer/

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.

Bulldozer driving


Attached Files

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. Model structure diagram


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

Bulldozer from .agx file

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

Bulldozer from .agx file with geometries


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 1 in the viewer to reload the scene with the added terrain. Press E to 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:

Drag the model with your mouse to see that the terrain is active.


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:

Bulldozer with 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:

Bulldozer with tracks


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:

Bulldozer with OpenPLX shovels

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

Ripper interacting with 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:

Bulldozer driving


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!