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

OpenPLX Tutorial 01 — Boxtruck

This tutorial will guide you through setting up a basic boxtruck simulation using OpenPLX. The goal is to introduce you to some of the core concepts of OpenPLX, including setting up rigid bodies, defining geometries, and creating interactive elements within a simulation. By the end of this tutorial, you will be familiar with the following concepts:

This foundational setup will serve as a base for future tutorials, where we’ll expand on these basics to incorporate steering, a more realistic drivetrain, and other dynamic components.

Inroduction GIF

Attached files:

The files listed are available under data/openplx/tutorial/t01-boxtruck in your AGX installation.


Before You Get Started

Familiarity with the agxViewer keybindings will also enhance your experience in this tutorial. Here are some essential commands:

For a full list of viewer shortcuts, refer to the AGX Keybindings documentation.

To learn more about OpenPLX and its features, visit the OpenPLX Documentation page.


Step 1: Define the Simulation Components

In this tutorial, we will create a scene with a simple vehicle model driving across a plane. The vehicle, which we will call boxtruck, will consist of a chassis and four wheels. The wheels will all be based on the same wheel model.

BoxTruck Box Diagram

For now, the truck will have four forward-facing wheels, with motors applied to the front wheels for basic movement. Later tutorials will show you how to implement more complex controls and steering.


Step 2: Setting Up the Scene

Begin by creating a new file named boxtruck.openplx. This file will define the entire simulation scenario and should extend the Physics3D.System model. Add this line to initialize a basic scene:

Scene is Physics3D.System

As default, the viewer will always read the last definition in a file as the main scene.

To load the file, use the OpenPLX viewer tool by running the following command in your terminal:

agxViewer boxtruck.openplx

Whenever you make changes to the code, press 1 in the viewer to reload the scene with the latest version and Space to reset the viewport.


Step 3: Adding a Chassis

Now, add a basic chassis to the scene. The chassis, represented as a Rigid Body with Box Geometry, acts as the main body of the truck. Add the following code to define it:

Chassis is Physics3D.Bodies.RigidBody:
    geometry is Physics3D.Geometries.Box:
        size:
            x: 3.3
            y: 1.7
            z: 1.44
    inertia.mass: 1000

# Add the Chassis to the scene
Scene is Physics3D.System:
    chassis is Chassis

The chassis will fall down when you start the scene. Press E to play or pause the simulation.

You can modify the chassis size by adjusting the x, y, and z values under geometry.size.

If this is your first time seeing OpenPLX syntax this might be a lot to take in. It is perfectly reasonable to finish this tutorial with very little knowledge of the OpenPLX syntax, just follow the instructions and it should give you a basic understanding of the syntax. A more detailed syntax description is available in the syntax documentation.


Step 4: Adding a Ground Plane

To prevent the chassis from falling endlessly, add a static Ground Plane. This plane provides a stable surface for the truck to rest on.

#...Chassis definition...

GroundPlane is Physics3D.Bodies.RigidBody:
    geometry is Physics3D.Geometries.Box:
        size:
            x: 100
            y: 100
            z: 0.1
    is_dynamic: false  # Ground plane is static

#Add the GroundPlane to the Scene
Scene is Physics3D.System:
    chassis is Chassis:
        kinematics.local_transform.position.z: 1.1 #Position the Chassis above the plane
    ground is GroundPlane

The chassis should now land on the ground plane, which remains stationary due to the is_dynamic: false setting.

BoxTruck on Plane


Step 5: Creating a Wheel Model

Define a Wheel model that will be reused for all four wheels. Each wheel will have a mass, cylindrical geometry, and a MateConnector to attach it to the chassis. A mate connector that is declared as an attribute in a rigid body defines a relative frame on which the constraints defined by a mate acts.

Wheel is Physics3D.Bodies.RigidBody:
    geometry is Physics3D.Geometries.Cylinder:
        radius: 0.26
        height: 0.165
    inertia.mass: 10

    connector is Physics3D.Interactions.MateConnector:
        .agx_debug_render_frame: true
        main_axis: { 0, -1, 0 }
        normal: { 1, 0, 0 }

To view the wheel model alone, load it in OpenPLX with agxViewer boxtruck.openplx --modelName Wheel -p. The -p flag starts the simulation paused, allowing you to inspect components without simulation dynamics.

In tutorial t02 - Introduction to automatic assembly you will learn more about how to model articulated systems with rigid bodies and mates using mate connectors. The built in SNAP algorithm will automatically assemble all rigid bodies that are interacting through mates.

Wheel preview


Step 6: Add Connectors to the Chassis

To make it possible to attach the wheels to the chassis, add four Mate Connectors where each wheel will connect.

Chassis is Physics3D.Bodies.RigidBody:
    geometry is Physics3D.Geometries.Box:
        size:
            x: 3.3
            y: 1.7
            z: 1.44
    inertia.mass: 1000
    # Connector offsets to position each wheel connector relative to chassis geometry
    connector_offsets is Math.Vec3:
        x: geometry.size.x * 0.31
        y: geometry.size.y * 0.44
        z: geometry.size.z * 0.48

    # Define connectors for each wheel, specifying positions and orientations
    front_left_connector is Physics3D.Interactions.MateConnector:
        .agx_debug_render_frame: true
        position:
            x: -connector_offsets.x
            y: -connector_offsets.y
            z: -connector_offsets.z
        main_axis: { 0, -1, 0 }
        normal: { 1, 0, 0 }

    front_right_connector is Physics3D.Interactions.MateConnector:
        .agx_debug_render_frame: true
        position:
            x: -connector_offsets.x
            y: connector_offsets.y
            z: -connector_offsets.z
        main_axis: { 0, 1, 0 }
        normal: { 1, 0, 0 }

    rear_left_connector is Physics3D.Interactions.MateConnector:
        .agx_debug_render_frame: true
        position:
            x: connector_offsets.x
            y: -connector_offsets.y
            z: -connector_offsets.z
        main_axis: { 0, -1, 0 }
        normal: { 1, 0, 0 }

    rear_right_connector is Physics3D.Interactions.MateConnector:
        .agx_debug_render_frame: true
        position:
            x: connector_offsets.x
            y: connector_offsets.y
            z: -connector_offsets.z
        main_axis: { 0, 1, 0 }
        normal: {1, 0, 0 }

These connectors ensure each wheel is correctly aligned with the chassis.

Box with Connectors

Step 7: Build the BoxTruck

Now, we are going to combine the Chassis model with four instances of the Wheel model into a new model called BoxTruck. We can combine multiple rigid bodies into a Physics3D.System, which can then be added to the Physics3D.System of the main scene.

Attach the wheels to the chassis using hinge joints, allowing each wheel to rotate freely.

BoxTruck is Physics3D.System:
    chassis is Chassis:
        kinematics.local_transform.position.z: 1.1  # Set initial height

    # Define wheel instances for each corner
    front_left_wheel is Wheel
    front_right_wheel is Wheel
    rear_left_wheel is Wheel
    rear_right_wheel is Wheel

    # Attach wheels to chassis with hinge joints
    front_left_hinge_joint is Physics3D.Interactions.Hinge:
        connectors: [chassis.front_left_connector, front_left_wheel.connector]
    front_right_hinge_joint is Physics3D.Interactions.Hinge:
        connectors: [chassis.front_right_connector, front_right_wheel.connector]

    rear_left_hinge_joint is Physics3D.Interactions.Hinge:
        connectors: [chassis.rear_left_connector, rear_left_wheel.connector]
    rear_right_hinge_joint is Physics3D.Interactions.Hinge:
        connectors: [chassis.rear_right_connector, rear_right_wheel.connector]

In your scene, replace the Chassis with the complete BoxTruck model.

Scene is Physics3D.System:
    boxTruck is BoxTruck
    ground is GroundPlane

Reload the simulation and confirm that the wheels are attached to the chassis.

BoxTruck Geometry

Step 8: Disable Collisions Between Chassis and Wheels

Prevent unintended collisions between the wheels and the chassis by using a CollisionGroup and DisableCollisionPair.

BoxTruck is Physics3D.System:
    #... Previous BoxTruck defintion...

    chassis_wheels_collision_group is Simulation.CollisionGroup:
        bodies: [chassis, front_left_wheel, front_right_wheel, rear_left_wheel, rear_right_wheel]

    disable_wheel_chassis_collisions is Simulation.DisableCollisionPair:
        group1: chassis_wheels_collision_group
        group2: chassis_wheels_collision_group

Disabling these collisions avoids simulation issues, as the wheels and chassis are part of the same assembly.


Step 9: Add Motors

Attach a Motor to each of the front wheels to provide forward motion. These motors will drive the front wheels at a set speed.

BoxTruck is Physics3D.System:
    #... Previous BoxTruck definition...

    # Define drive motors to apply rotation to front wheels
    front_left_drive_motor is Physics3D.Interactions.RotationalVelocityMotor:
        connectors: front_left_hinge_joint.connectors
        target_speed: 1  # Speed for left wheel

    front_right_drive_motor is Physics3D.Interactions.RotationalVelocityMotor:
        connectors: front_right_hinge_joint.connectors
        target_speed: 1  # Speed for right wheel

The motors will rotate the front wheels at a constant speed. Later, you can adjust the speed dynamically using input controls.

BoxTruck with motors


Step 10: Add Friction Materials

Define Materials and a Surface Contact Model between the wheels and the ground to improve grip, making the simulation more realistic by reducing slip.

Wheel Material:

const WheelMaterial is Physics.Geometries.Material:
    unique_name: "Tutorial01_BoxTruck_WheelContactMaterial"
    density: 1000

Ground Material:

const GroundMaterial is Physics.Geometries.Material:
    unique_name: "Tutorial01_BoxTruck_GroundMaterial"
    density: 10

Surface Contact Model between Wheel and Ground:

# Define friction model between wheels and ground for realistic interactions
WheelGroundFrictionModel is Physics.Interactions.SurfaceContact.Model:
    material_1: WheelMaterial
    material_2: GroundMaterial
    tangential_restitution: 0.0
    normal_restitution: 0.0
    friction becomes Physics.Interactions.Dissipation.DryConstantNormalForceFriction:
        normal_force: 1000
        coefficient: 1.0

Assign these materials to the wheel and ground geometries and attach the friction model.

Assign the Wheel Material:

Wheel is Physics3D.Bodies.RigidBody:
    geometry is Physics3D.Geometries.Cylinder:
        radius: 0.26
        height: 0.165
        material: WheelMaterial

    #...Rest of Wheel Definition...

Assign the Ground Material:

GroundPlane is Physics3D.Bodies.RigidBody:
    geometry is Physics3D.Geometries.Box:
        size:
            x: 100
            y: 100
            z: 0.1
        material: GroundMaterial
    is_dynamic: false  # Ground plane is static

Add the Surface Contact Model to the Scene:

Scene is Physics3D.System:
    boxTruck is BoxTruck
    ground is GroundPlane

    wheel_ground_friction is WheelGroundFrictionModel

These friction materials help the truck handle better on the ground surface. You can try modifying the friction model parameters further.

Materials are prefixed with the const keyword making them global in scope, read more here Const models. The surface contact models however are tied to a specific Physics3D.System.


Step 11: Set Up Controls

To control the truck using keyboard inputs, create a Python script called boxtruck.py that interacts with the OpenPLX signaling system.

The signals for the wheel motors are already defined in the RotationalVelocityMotors. You can access them through "boxTruck.front_left_drive_motor.target_angular_velocity_input" and vice versa.

Use the provided Python script to add forward and backward control for the truck:

"""
Controller script for boxtruck.openplx
"""
import os
from agxPythonModules.utils.environment import init_app, application, root
from agxPythonModules.utils.callbacks import KeyboardCallback
import openplx
from openplx.Physics.Signals import RealInputSignal
import agx
import agxOSG
import agxSDK

# Class to manage boxtruck maneuvering using input signals
class BoxTruckManeuvering:

    def __init__(self, input_queue, front_left_motor_velocity_input: openplx.Physics.Signals.Input,
                 front_right_motor_velocity_input: openplx.Physics.Signals.Input):
        """
        Initialize the boxtruck maneuvering class with input signals for motor control.

        :param input_queue: Queue to send signals to the simulation
        :param front_left_motor_velocity_input: Input signal for the front left motor
        :param front_right_motor_velocity_input: Input signal for the front right motor
        """
        super().__init__()
        self.input_queue = input_queue
        self.front_left_motor_velocity_input = front_left_motor_velocity_input
        self.front_right_motor_velocity_input = front_right_motor_velocity_input
        self.target_velocity = 0  # Default target velocity
        self.amplitude = 3  # Speed amplitude for the motors

    def send_signals(self):
        """
        Send motor velocity signals to the input queue.
        """
        # Forward signal for the left motor
        self.input_queue.send(
            RealInputSignal.create(
                self.target_velocity, self.front_left_motor_velocity_input))
        # Reverse signal for the right motor
        self.input_queue.send(
            RealInputSignal.create(
                -self.target_velocity, self.front_right_motor_velocity_input))

    def forward(self):
        """
        Set target velocity for forward movement and send signals.
        """
        self.target_velocity = self.amplitude
        self.send_signals()

    def backward(self):
        """
        Set target velocity for backward movement and send signals.
        """
        self.target_velocity = -self.amplitude
        self.send_signals()

    def stop(self):
        """
        Stop the vehicle by setting the target velocity to zero.
        """
        self.target_velocity = 0
        self.send_signals()

    def handle_forward(self, key) -> bool:
        """
        Handle forward key press/release events.

        :param key: Key event object
        :return: True if key handling was successful
        """
        if key.down:
            self.forward()  # Move forward on key press
        else:
            self.stop()  # Stop on key release

    def handle_backward(self, key) -> bool:
        """
        Handle backward key press/release events.

        :param key: Key event object
        :return: True if key handling was successful
        """
        if key.down:
            self.backward()  # Move backward on key press
        else:
            self.stop()  # Stop on key release

    def setup_keyboard_listener(self):
        """
        Set up keyboard listeners for forward and backward controls.
        """
        KeyboardCallback.bind(name="forward", key=agxSDK.GuiEventListener.KEY_Up, callback=self.handle_forward)
        KeyboardCallback.bind(name="backward", key=agxSDK.GuiEventListener.KEY_Down, callback=self.handle_backward)

def initialize_scene_decorator():
    camera_data = application().getCameraData()
    camera_data.eye = agx.Vec3(-12.8402, 18.6618, 5.0588)
    camera_data.center = agx.Vec3(1.0282, -0.5451, 2.0728)
    camera_data.up = agx.Vec3(0.0, 0.0, 0.9917)
    camera_data.nearClippingPlane = 0.01
    camera_data.farClippingPlane = 500
    application().applyCameraData(camera_data)
    application().getSceneDecorator().setEnableShadows(True)
    application().getSceneDecorator().setShadowMethod(
        agxOSG.SceneDecorator.SOFT_SHADOWMAP
    )
    application().getSceneDecorator().setEnableShaderState(True)

    application().getSceneDecorator().setEnableCalculateLightPositions(agxOSG.SceneDecorator.LIGHT0, False)
    application().getSceneDecorator().setEnableCalculateLightPositions(agxOSG.SceneDecorator.LIGHT1, False)
    application().getSceneDecorator().setEnableCalculateLightPositions(agxOSG.SceneDecorator.LIGHT2, False)

    application().getSceneDecorator().getLightSource(agxOSG.SceneDecorator.LIGHT0).setPosition(agx.Vec4(10, 10, 10, 1))
    application().getSceneDecorator().getLightSource(agxOSG.SceneDecorator.LIGHT1).setPosition(agx.Vec4(-10, 10, 10, 1))
    application().getSceneDecorator().getLightSource(agxOSG.SceneDecorator.LIGHT2).setPosition(agx.Vec4(10, 19, 10, 1))

# Function to build the simulation scene
def buildScene(): # pylint: disable=invalid-name
    """
    Load the OpenPLX scene and set up the boxtruck maneuvering system.
    """

    initialize_scene_decorator()

    # Get the current script directory and load the OpenPLX scene
    file_dir = os.path.dirname(os.path.abspath(__file__))
    result = application().loadOpenPlxFile(os.path.join(file_dir, "boxtruck.openplx"), root())
    openplx_scene = result.scene()

    # Retrieve motor input objects from the scene
    assert(front_left_motor_velocity_input := openplx_scene.getObject(
        "boxTruck.front_left_drive_motor.target_angular_velocity_input"))
    assert(front_right_motor_velocity_input := openplx_scene.getObject(
        "boxTruck.front_right_drive_motor.target_angular_velocity_input"))


    # Initialize boxtruck maneuvering and set up input listeners
    boxtruck_maneuvering = BoxTruckManeuvering(result.getInputSignalQueue(), front_left_motor_velocity_input,
                                     front_right_motor_velocity_input)
    boxtruck_maneuvering.setup_keyboard_listener()

if __name__ == "__main__":
    init = init_app(
        name="__main__",
        scenes=[(buildScene, "1")],
        autoStepping=True)

After writing the script, you can run it with:

python boxtruck.py

Running this script allows you to control the truck using the up and down arrow keys.


Step 12: Add Visuals

Lastly, add visual components to the truck and ground to enhance the simulation’s appearance. Use the visuals attached to this tutorial to turn your boxtruck into an actual truck:

The files listed are available under data/openplx/tutorial/t01-boxtruck in your AGX installation.

Add an offset to the chassis visuals to position it in the right spot, and modify the chassis and wheel geometry to match the visuals.

Chassis:

const ChassisVisualMaterial is Visuals.Materials.TextureMaterial:
    path: $"{AGX_DATA_DIR}/models/openplx/Chassis.png"

Chassis is Physics3D.Bodies.RigidBody:
    visual is Visuals.Geometries.ExternalTriMeshGeometry:
        path: @"chassis.obj"
        material: ChassisVisualMaterial
        local_transform.position: { 0, 0, -0.72 } # Offset the visual to match the collision geometry

    geometry is Physics3D.Geometries.Box:
        size:
            x: 3.25
            y: 1.2
            z: 1.5
    inertia.mass: 1000

    #...MateConnectors...

Wheel:

const WheelVisualMaterial is Visuals.Materials.TextureMaterial:
    path: $"{AGX_DATA_DIR}/models/openplx/Wheel.png"

Wheel is Physics3D.Bodies.RigidBody:
    visual is Visuals.Geometries.ExternalTriMeshGeometry:
        path: @"wheel.obj"
        material: WheelVisualMaterial
    geometry is Physics3D.Geometries.Cylinder:
        radius: 0.32
        height: 0.25
        material: WheelMaterial  # Use wheel contact material
    inertia.mass: 10

    #...MateConnector...

Ground:

const GroundVisualMaterial is Visuals.Materials.TextureMaterial:
    path: $"{AGX_DATA_DIR}/models/openplx/Ground.png"
    repeat: true
    scale_u: 1
    scale_v: 1

GroundPlane is Physics3D.Bodies.RigidBody:
    visual is Visuals.Geometries.ExternalTriMeshGeometry:
        path: @"ground.obj"
        material: GroundVisualMaterial
    geometry is Physics3D.Geometries.Box:
        size:
            x: 100
            y: 100
            z: 0.1
    is_dynamic: false  # Ground plane is static

To show or hide collision geometries, press G.


This completes your Boxtruck tutorial.

BoxTruck Box Diagram


In following tutorials, you will learn to