Modular Vehicles Tutorial 1: Creating a Vehicle Topology

This tutorial includes video content: 


In this first module, we will setup the file structure as well as the topology of the drivetrain of an 8x8 land vehicle.

The goal is to fully create a modular vehicle without using templates. This will give you a better understanding of the structure of modular vehicles, allowing you to create complex powertrain systems.

Prerequisites

You need to have Vortex Studio and the Vortex Studio Samples and Demo Scenes installed to be able to follow all steps in this tutorial : https://www.cm-labs.com/downloads/

Creating the vehicle file structure

  1. In your working directory, create a folder named AWD_8x8
  2. Copy the Components folder (C:\CM Labs\Vortex Studio Content <version>\Samples\Vehicle Systems) to this new folder.
  3. Create a Resources folder. We'll use this folder later in the tutorial.
  4. In Vortex Studio, Create an Assembly. Save it in the AWD_8x8 folder and name it AWD_8x8.vxassembly.

Creating the vehicle topology

  1. Open AWD_8x8.vxassembly, created in the previous section.
  2. From the Basics section of the Toolbox, insert Assemblies From Files
  3. Select Chassis.vxassembly from ...\Components\Chassis
  4. Repeat this process to add all the parts of the vehicle's powertrain:
    1. Engine (Advanced)
    2. Torque Converter (Advanced)
    3. Automatic Transmission
    4. Differentials (7)
    5. Wheels (8)
  5. Rename each wheels to identify their positions on the LAV ("FL" for Front Left, "FML" for Front Middle Left, etc.).


  6. From the Basics section of the Toolbox, insert a Connection Container. Rename it "Chassis Connections"
  7. Insert the Chassis Part Output from the Chassis Attachments VHL in the Chassis Assembly.
  8. For each other component, find their Attachments VHL and insert the Chassis Part Input, connecting it to the Chassis Part Output of the Chassis assembly


  9. From the Basics section of the Toolbox, insert a Connection Container. Rename it "Vehicle Topology"
  10. Insert all Shaft Part Inputs and Parameters from the Engine, Torque Converter, Transmission, Differentials and Wheels Attachments VHL.
  11. Connect Inputs and Parameters following to the topology below:


  12. Insert Shaft RPM and Shaft Speed from the Engine Interface and Torque Converter Coupling Interface.
  13. Insert Input Shaft Speed from the Transmission Interface and Torque Converter Coupling Interface.
  14. Make these connections:

Creating a Wheel Positioner

  1. From the Simulation section of the Toolbox, insert a Dynamics Script.
  2. Close the resulting window and rename it "Wheel Positioner"
  3. In the Code field of the extension, copy the following Python Code.

    Component Positioner Code
    from Vortex import *
    
    def on_simulation_start(self):
    
        create_input(self, 'Enable', Types.Type_Bool).setDescription("True to update positions")
    
        create_parameter(self, 'Wheel Position Front', Types.Type_VxReal).setDescription("Position of front axle")
        create_parameter(self, 'Wheel Position Front Middle', Types.Type_VxReal).setDescription("Position of front axle")
        create_parameter(self, 'Wheel Position Rear Middle', Types.Type_VxReal).setDescription("Position of front axle")
        create_parameter(self, 'Wheel Position Rear', Types.Type_VxReal).setDescription("Position of rear axle")
        create_parameter(self, 'Wheel Spacing', Types.Type_VxReal).setDescription("Distance between left and right wheels")
        create_parameter(self, 'Wheel Height', Types.Type_VxReal).setDescription("Vertical position of wheels")
    
        create_output(self, 'Wheel FL Transform', Types.Type_VxMatrix44)
        create_output(self, 'Wheel FR Transform', Types.Type_VxMatrix44)
        create_output(self, 'Wheel FML Transform', Types.Type_VxMatrix44)
        create_output(self, 'Wheel FMR Transform', Types.Type_VxMatrix44)
        create_output(self, 'Wheel RML Transform', Types.Type_VxMatrix44)
        create_output(self, 'Wheel RMR Transform', Types.Type_VxMatrix44)
        create_output(self, 'Wheel RL Transform', Types.Type_VxMatrix44)
        create_output(self, 'Wheel RR Transform', Types.Type_VxMatrix44)
    
    def paused_update(self):
        config_update(self)
    
    def config_update(self):
        # Skip update if disabled
        if not self.inputs.Enable.value:
            return
    
        # Place wheels
        pos = VxVector3(self.parameters.Wheel_Position_Front.value, self.parameters.Wheel_Spacing.value/2.0, self.parameters.Wheel_Height.value)
        #Front
        self.outputs.Wheel_FL_Transform.value = createTranslation(pos)
        pos.y = -pos.y
        self.outputs.Wheel_FR_Transform.value = createTranslation(pos)
        #Front Middle
        pos.x = self.parameters.Wheel_Position_Front_Middle.value
        self.outputs.Wheel_FMR_Transform.value = createTranslation(pos)
        pos.y = -pos.y
        self.outputs.Wheel_FML_Transform.value = createTranslation(pos)
        #Rear Middle
        pos.x = self.parameters.Wheel_Position_Rear_Middle.value
        self.outputs.Wheel_RML_Transform.value = createTranslation(pos)
        pos.y = -pos.y
        self.outputs.Wheel_RMR_Transform.value = createTranslation(pos)
        #Rear
        pos.x = self.parameters.Wheel_Position_Rear.value
        self.outputs.Wheel_RR_Transform.value = createTranslation(pos)
        pos.y = -pos.y
        self.outputs.Wheel_RL_Transform.value = createTranslation(pos)
    
    def create_output(extension, name, o_type, default_value=None):
        """Create output field with optional default value, reset on every simulation run."""
        if extension.getOutput(name) is None:
            extension.addOutput(name, o_type)
        if default_value is not None:
            extension.getOutput(name).value = default_value
        return extension.getOutput(name)
    
    def create_parameter(extension, name, p_type, default_value=None):
        """Create parameter field with optional default value set only when the field is created."""
        if extension.getParameter(name) is None:
            field = extension.addParameter(name, p_type)
            if default_value is not None:
                field.value = default_value
        return extension.getParameter(name)
    
    def create_input(extension, name, i_type, default_value=None):
        """Create input field with optional default value set only when the field is created."""
        if extension.getInput(name) is None:
            field = extension.addInput(name, i_type)
            if default_value is not None:
                field.value = default_value
        return extension.getInput(name)

    In order for the Inputs and Outputs to be added to the Script extension, you need to Start (F7) the simulation at least once and select the script extension again.

  4. From the Basics section of the Toolbox, insert a Connection Container. Rename it "Wheel Positioner Connections"
  5. Connect the Wheel Transform Outputs from the script to the Local Transform of each wheel assembly.
  6. From the Basics section of the Toolbox, insert a Folder. Rename it "Components". Insert all of the other extensions into this folder.
  7. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Component Configuration".

  8. Drag and drop the Parameters of the Wheel Positioner script into its Parameters section.
  9. Add the Body Attachment Point Parameter from the Chassis Attachments VHL in the Chassis.

Adding Wheel and Suspension Tuning Interface

  1. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Wheel and Suspension Tuning"
  2. Add and group all of the following Inputs from the Wheel Interface VHL from each wheel in the Wheel and Suspension Tuning interface.
    • Radius
    • Width
    • Mass
    • Suspension Stiffness
    • Suspension Damping
    • Type Type
    • Tire Stiffness

Adding Engine Tuning Interface

  1. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Engine Tuning".
  2. From the Engine Interface VHL, insert the following Inputs and Parameters in the Parameters section of this new VHL.
    • Torque Scale
    • Braking Torque Scale
    • Shaft Mass
    • Shaft Inertia
    • Torque Table
    • Braking Torque Table

Adding Torque Converter Tuning Interface

  1. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Torque Converter Tuning".
  2. From the Coupling Interface VHL, insert the following Inputs and Parameters in the Parameters section of this new VHL.
    • Stall RPM
    • Stall Torque
    • All Parameters.

Adding Transmission Tuning Interface

  1. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Transmission Tuning".
  2. From the Transmission Interface VHL, insert the following Inputs and Parameters in the Parameters section of this new VHL.
    • All Inputs except Gear Selection, Min Gear Selection, Park, Input Shaft Speed, Throttle.
    • All Parameters.

Adding Differentials Tuning Interface

  1. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Differentials Tuning"
  2. Add and group all of the following Inputs and Parameters from the Differential Interface VHL from each differential in the Wheel and Differentials Tuning interface.
    • Lock Torque
    • Gear Ratio
    • Shaft Mass
    • Shaft Intertia

Creating an Ackerman Steering Controller

  1. From the Simulation section of the Toolbox, insert a Dynamics Script.
  2. Close the resulting window and rename it "Steering Controller"
  3. In the Code field of the extension, copy the following Python Code.

    Steering Controller
    from Vortex import *
    from math import *
    
    def on_simulation_start(self):
    
        self.Steering_Input = create_input(self, "Steering Input", Types.Type_VxReal)
        self.Front_Axle_Position = create_input(self, "Front Axle Position", Types.Type_VxReal)
        self.Front_Middle_Axle_Position = create_input(self, "Front Middle Axle Position", Types.Type_VxReal)
        self.Rear_Middle_Axle_Position = create_input(self, "Rear Middle Axle Position", Types.Type_VxReal)
        self.Rear_Axle_Position = create_input(self, "Rear Axle Position", Types.Type_VxReal)
        self.Width = create_input(self, "Width", Types.Type_VxReal)
        self.Max_Angle = create_input(self, "Max Angle", Types.Type_VxReal)
    
        self.Angle_FL = create_output(self, "Angle FL", Types.Type_VxReal)
        self.Angle_FR = create_output(self, "Angle FR", Types.Type_VxReal)
        self.Angle_FML = create_output(self, "Angle FML", Types.Type_VxReal)
        self.Angle_FMR = create_output(self, "Angle FMR", Types.Type_VxReal)
    
    def pre_step(self):
    
        steeringInput = clamp(self.Steering_Input.value, -1.0, 1.0)
        rear_axle_pos = self.Rear_Axle_Position.value + (self.Rear_Middle_Axle_Position.value - self.Rear_Axle_Position.value)
        angle = steeringInput * self.Max_Angle.value
        pos_front = self.Front_Axle_Position.value - rear_axle_pos
        pos_middle = self.Front_Middle_Axle_Position.value - rear_axle_pos
        pos_lat = self.Width.value / 2.0
    
    
        self.outputs.Angle_FL.value = compute_angle(pos_front, pos_lat, angle)
        self.outputs.Angle_FR.value = compute_angle(pos_front, -pos_lat, angle)
        self.outputs.Angle_FML.value = compute_angle(pos_middle, pos_lat, angle)
        self.outputs.Angle_FMR.value = compute_angle(pos_middle, -pos_lat, angle)
    
    def compute_angle(pos_long, pos_lat, angle):
    
        outputAngle = 0.0
    
        tanAngle = tan(angle)
        if( abs(tanAngle) > 0.0000001 ):
            radius = pos_long / tanAngle
            divider = radius + pos_lat
            if( abs(divider) > 0.0000001 ):
                outputAngle = atan(pos_long/divider)
        return outputAngle;
    
    def clamp(value, min_value, max_value):
        """Return a bounded value."""
        return min(max(value, min_value), max_value)
    
    def create_output(extension, name, o_type, default_value=None):
        """Create output field with optional default value, reset on every simulation run."""
        if extension.getOutput(name) is None:
            extension.addOutput(name, o_type)
        if default_value is not None:
            extension.getOutput(name).value = default_value
        return extension.getOutput(name)
    
    def create_parameter(extension, name, p_type, default_value=None):
        """Create parameter field with optional default value set only when the field is created."""
        if extension.getParameter(name) is None:
            field = extension.addParameter(name, p_type)
            if default_value is not None:
                field.value = default_value
        return extension.getParameter(name)
    
    def create_input(extension, name, i_type, default_value=None):
        """Create input field with optional default value set only when the field is created."""
        if extension.getInput(name) is None:
            field = extension.addInput(name, i_type)
            if default_value is not None:
                field.value = default_value
        return extension.getInput(name)
  4. Edit the Wheel Positioning VHL. 

  5. From the Steering Control script properties, attach the following inputs to the corresponding parameter in the VHL:
    • Front Axle Position
    • Front Middle Axle Position
    • Rear Middle Axle Position
    • Rear Axle Position
    • Width
    • Max Angle (rename it "Max Steering Angle" in the VHL)
  6. From the Basics section of the Toolbox, insert a Connection Container. Rename it "Steering Connections".
  7. Insert all Outputs from the Steering Controller script.
  8. Connect the outputs to the Steering Angle Input of the corresponding Wheel Interface VHL.

Creating a Braking Controller

  1. From the Simulation section of the Toolbox, insert a Dynamics Script.
  2. Close the resulting window and rename it "Braking Controller".
  3. In the Code field of the extension, copy the following Python Code.

    Braking Controller
    # This script is calculates the front and rear braking torques based on max torque and bias
    from Vortex import *
    
    def on_simulation_start(extension):
    
        extension.Brake_Input = create_input(extension, "Brake Input", Types.Type_VxReal, 0)
        extension.Max_Torque = create_input(extension, "Max Torque", Types.Type_VxReal, 1000)
        extension.Front_Rear_Bias = create_input(extension, "Front Rear Bias", Types.Type_VxReal, 0.6)
    
        extension.Braking_Torque_F = create_output(extension, "Braking Torque F", Types.Type_VxReal)
        extension.Braking_Torque_R = create_output(extension, "Braking Torque R", Types.Type_VxReal)
        
    def pre_step(extension):
        extension.Braking_Torque_F.value = extension.Max_Torque.value * extension.Brake_Input.value
        extension.Braking_Torque_R.value = (extension.Max_Torque.value * extension.Brake_Input.value 
                                       * (1 - extension.Front_Rear_Bias.value) / extension.Front_Rear_Bias.value)
    
    
    def clamp(value, min_value, max_value):
        """Return a bounded value."""
        return min(max(value, min_value), max_value)
    
    def create_output(extension, name, o_type, default_value=None):
        """Create output field with optional default value, reset on every simulation run."""
        if extension.getOutput(name) is None:
            extension.addOutput(name, o_type)
        if default_value is not None:
            extension.getOutput(name).value = default_value
        return extension.getOutput(name)
    
    def create_parameter(extension, name, p_type, default_value=None):
        """Create parameter field with optional default value set only when the field is created."""
        if extension.getParameter(name) is None:
            field = extension.addParameter(name, p_type)
            if default_value is not None:
                field.value = default_value
        return extension.getParameter(name)
    
    def create_input(extension, name, i_type, default_value=None):
        """Create input field with optional default value set only when the field is created."""
        if extension.getInput(name) is None:
            field = extension.addInput(name, i_type)
            if default_value is not None:
                field.value = default_value
        return extension.getInput(name)
  4. From the Basics section of the Toolbox, insert a Connection Container. Rename it "Braking Connections".

  5. Insert all Outputs from the Braking Controller script.

  6. Connect the Braking Torque Input of the front wheels Wheel Interface to Braking Torque F.

  7. Connect the Braking Torque Input of the rear wheels Wheel Interface to Braking Torque R.
  8. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Braking Tuning"
  9. Add the following Inputs from the Braking Controller script:
    • Max Torque
    • Front Rear Bias

Adding a Vehicle Control Interface

  1. From the Simulation section of the Toolbox, insert a VHL Interface. Rename it "Vehicle Control".
  2. Expose the following Inputs from the corresponding component:
    • Engine → Engine Interface
      • Engine Running
      • Throttle
    • Steering Controller
      • Steering Input
    • Braking Controller
      • Brake Input
    • Automatic Transmission → Transmission Interface
      • Gear Selection
      • Park
      • Throttle (linked to Engine Throttle)
    • Differentials Tuning
      • Lock Torque