Describe the Bug
A clear and concise description of what the bug is.
“When creating a rover.proto using the Car proto, after frequent left and right turns, the two front wheels gradually become misaligned. I tried adjusting many parameters (e.g., maxSteeringTorque, wheelsDampingConstant, suspensionFrontSpringConstant, suspensionFrontDampingConstant .etc), but it didn’t help.
Also, when setting coulombFriction in contactProperties: if the value is relatively low, the rover moves very slowly and cannot reach the max speed. If coulombFriction is relatively high, the speed improves, but once turning or braking, the rover spins out of control. How can I prevent the rover from spinning out during turns or braking while still allowing it to reach a relatively high speed (i.e., the set max speed)?”
track.wbt:
#VRML_SIM R2025a utf8
# Racing Sim — Base World (no track geometry).
#
# Track geometry is provided separately by the user (e.g., as a CadShape STL
# or custom Solid nodes). Place your track nodes inside this file.
#
# Timing system (race_supervisor) reads track_meta.json for start/checkpoint
# line definitions. Update that JSON when you update the track.
IMPORTABLE EXTERNPROTO "../protos/Rover.proto"
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2025a/projects/objects/backgrounds/protos/TexturedBackground.proto"
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2025a/projects/objects/backgrounds/protos/TexturedBackgroundLight.proto"
WorldInfo {
info [ "Racing Sim — user-provided track" ]
title "Racing Track"
basicTimeStep 2
# ------------------------------------------------------------------
# Friction: "road" is a tag used by the floor Solid below.
# High coulombFriction prevents wheel slip on the ground.
# "vehicleWheel" matches the default wheel contact material in
# AckermannVehicle / VehicleWheel.
# If your imported track uses a different contactMaterial string,
# replace "road" with that string in both the floor Solid and here.
# ------------------------------------------------------------------
contactProperties [
ContactProperties {
material1 "road"
material2 "default"
coulombFriction 2.1
forceDependentSlip 0.0
softCFM 0.001
}
]
}
Viewpoint {
fieldOfView 0.8
orientation 0.35 -0.65 0.42 1.1
position 0 -15 30
near 0.2
far 600
}
TexturedBackground { }
TexturedBackgroundLight { }
# ------------------------------------------------------------------
# Floor — large flat ground plane with high-friction contact material.
# Replace or remove this if your track provides its own drive surface.
# ------------------------------------------------------------------
Solid {
translation 0 0 -0.01
contactMaterial "road"
children [
Shape {
appearance PBRAppearance {
baseColor 0.25 0.30 0.25
roughness 0.9
metalness 0
}
geometry Box {
size 300 300 0.02
}
}
]
boundingObject Plane {
size 300 300
}
}
DirectionalLight {
direction -0.4 -0.8 -0.3
intensity 1.0
castShadows TRUE
}
DirectionalLight {
direction 0.3 -0.4 -0.6
intensity 0.4
}
# ==================================================================
# RACE SUPERVISOR — spawns cars, handles timing, keyboard input
# ==================================================================
Robot {
name "supervisor"
controller "race_supervisor"
supervisor TRUE
children [
Emitter {
name "supervisorEmitter"
channel 1
}
]
}
Rover.proto:
#VRML_SIM R2025a utf8
# Rover PROTO — Ackermann-steering RC racing rover (~10 kg, 1/10 scale).
#
# Wraps Car PROTO with electric engine + propulsion (rear drive).
# Driver API: setSteeringAngle() (auto-Ackermann) + setThrottle() + setBrakeIntensity().
#
# Physical parameters (violent RC off-road buggy, brushless motor):
# - 0–120 km/h in ~3 s | ~5 kW motor | direct drive
# - chassis 0.6×0.2×0.1 m³ × density 833 ≈ 10 kg (+wheels ≈ 11 kg total)
#
# Sensors (all 640×480 for cameras):
# - Front RGB camera (planar, 60°)
# - 60-line LiDAR (360° horizontal × 75° vertical, cylindrical, top)
# - 2× 170° fisheye cameras (spherical, back-to-back atop LiDAR)
# - GPS / InertialUnit / Compass
# - Chassis TouchSensor (force-3d)
#
# Usage (in .wbt):
# IMPORTABLE EXTERNPROTO "../protos/Rover.proto"
# Spawn via Supervisor:
# root_children.importMFNodeFromString(-1, 'DEF racecar Rover { name "racecar" ... }')
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2025a/projects/vehicles/protos/abstract/Car.proto"
EXTERNPROTO "https://raw.githubusercontent.com/cyberbotics/webots/R2025a/projects/vehicles/protos/abstract/VehicleWheel.proto"
PROTO Rover [
field SFVec3f translation 0 0 0.08 # spawn position [m]
field SFRotation rotation 0 0 1 0 # spawn orientation (axis-angle)
field SFString name "racecar" # robot name (used for command routing)
field SFString controller "rover_controller" # controller program name
# ---- Chassis ----
field SFVec3f chassisSize 0.60 0.20 0.10 # length(x) × width(y) × height(z) [m]
field SFFloat chassisDensity 833.0 # [kg/m³] → mass ≈ 833×0.6×0.2×0.1 ≈ 10 kg
# ---- Ackermann steering ----
field SFFloat wheelbase 0.38 # front-rear axle distance [m]
field SFFloat trackWidth 0.36 # left-right wheel distance [m]. Must give clearance
# from chassis: half_track - wheel_radius > chassis_half_width.
# 0.40 → wheel inner edge at 0.12, chassis edge at 0.10, clearance 2cm.
field SFFloat maxSteeringAngle 0.7 # Hinge2Joint limit [rad] (~40°). Must be > inner-wheel
# Ackermann angle (0.672 rad for 30° command).
# Controller MAX_STEER=0.5236 is the commanded angle.
field SFFloat maxSteeringTorque 20.0 # steering motor torque limit [Nm].
# Scaled from Car default 10000 Nm by wheel inertia ratio.
# 100 Nm caused overshoot/oscillation (too high P gain).
# ---- Drive (Car PROTO electric engine, direct drive) ----
field SFFloat maxVelocity 33.0 # top speed [m/s] (~120 km/h)
field SFFloat time0To100 2.8 # 0→100 km/h time [s]
field SFFloat engineMaxTorque 20.0 # motor torque [Nm] (direct drive, no reduction)
field SFFloat engineMaxPower 5000.0 # motor power [W] (≈ 5 kW)
# ---- Wheels ----
field SFFloat wheelRadius 0.08 # tire outer radius [m]
field SFFloat wheelWidth 0.09 # tire width [m]; total mass ≈ chassis(~10kg) + 4wheels(~0.25kg) ≈ 11 kg
]
{
%<
// --- Vehicle geometry computed from fields ---
let wb = fields.wheelbase.value;
let trk = fields.trackWidth.value;
let wR = fields.wheelRadius.value;
let wW = fields.wheelWidth.value;
let chW = fields.chassisSize.value.x;
let chD = fields.chassisSize.value.y;
let chH = fields.chassisSize.value.z;
let clr = 0.05;
// Chassis Z offset from axle height (ground clearance + half chassis height - wheel radius)
let chZ = clr + chH * 0.5 - wR;
// Sensor placement (relative to chassis origin)
let camX = chW * 0.5;
let camZ = chZ + chH * 0.5 + 0.18;
let lidZ = chZ + chH * 0.5 + 0.10;
let fishZ = lidZ + 0.06;
>%
Car {
translation IS translation
rotation IS rotation
name IS name
controller IS controller
model "rover"
# --- Drive layout: rear-wheel drive, electric motor (torque = throttle × engineMaxTorque) ---
type "propulsion"
engineType "electric"
# --- Ackermann (inherited from Car/AckermannVehicle) ---
# maxSteeringAngle=0.7 rad is the Hinge2Joint limit for the inner wheel.
# Commanded angle via setSteeringAngle() is 30° (0.5236 rad, MAX_STEER in controller).
# Ackermann: inner wheel ≈ 38.5° (0.672 rad) for 30° command → requires limit > 0.672.
trackFront IS trackWidth
trackRear IS trackWidth
wheelbase IS wheelbase
maxSteeringAngle IS maxSteeringAngle
minSteeringAngle %<= -fields.maxSteeringAngle.value >%
maxSteeringTorque IS maxSteeringTorque
# --- Engine (Car PROTO manages motors internally; Driver API controls via setThrottle) ---
maxVelocity IS maxVelocity
time0To100 IS time0To100
engineMaxTorque IS engineMaxTorque
engineMaxPower IS engineMaxPower
engineMinRPM 100
engineMaxRPM 20000
engineSound "none"
engineFunctionCoefficients 0 0 0 # unused for electric
gearRatio [-1.0, 1.0] # [-reverse, +forward] direct drive
hybridPowerSplitRatio 0
hybridPowerSplitRPM 3000
# --- Brakes (Car PROTO brake model; Driver API controls via setBrakeIntensity) ---
brakeCoefficient 800
# --- Suspension: scaled for 10 kg RC car ---
# Spring 20000 N/m → static deflection ≈ (10×9.81)/20000 ≈ 5 mm
# Damper 800 Ns/m → near-critical damping
# wheelsDampingConstant=0.01: wheel SPIN axis damping. Keep low —
# higher values make the car sluggish (resists wheel rotation).
# Wheel tilt is NOT caused by this parameter (see maxSteeringTorque).
wheelsDampingConstant 0.01
suspensionFrontSpringConstant 20000
suspensionFrontDampingConstant 800
suspensionRearSpringConstant 20000
suspensionRearDampingConstant 800
# --- Vehicle physics (dynamics mode required for Hinge2Joint) ---
boundingObject Pose {
translation %<= wb * 0.5 >% 0 %<= chZ >%
children [
Box { size %<= chW >% %<= chD >% %<= chH >% }
]
}
physics Physics { density IS chassisDensity }
# ====================================================================
# Extension slot — sensor payload
# ====================================================================
extensionSlot [
Solid {
translation %<= wb * 0.5 >% 0 %<= chZ >%
name "chassis"
children [
# --- Receiver (channel 1, receives commands from Supervisor) ---
Receiver {
name "receiver"
channel 1
}
# --- Chassis visual (blue box) ---
Shape {
appearance PBRAppearance {
baseColor 0.1 0.2 0.8
roughness 0.35
metalness 0.4
}
geometry Box { size %<= chW >% %<= chD >% %<= chH >% }
}
# --- Windshield visual (semi-transparent) ---
Transform {
translation %<= chW * 0.25 >% 0 %<= chH * 0.3 >%
children [
Shape {
appearance PBRAppearance {
baseColor 0.5 0.7 1.0
roughness 0.1
metalness 0.2
transparency 0.4
}
geometry Box {
size %<= chW * 0.2 >% %<= chD * 0.85 >% %<= chH * 0.35 >%
}
}
]
}
# --- Chassis collision sensor (force-3d, triggers >0.5N) ---
TouchSensor {
name "touchSensorChassis"
type "force-3d"
lookupTable []
boundingObject Box { size %<= chW >% %<= chD >% %<= chH >% }
physics Physics { density 1 }
}
# --- Front RGB Camera (planar, 60° FOV, 640×480) ---
Camera {
translation %<= camX >% 0 %<= camZ >%
rotation 0 0 1 0
name "camera"
fieldOfView 1.0472
width 640
height 480
noise 0.01
}
# --- 60-line LiDAR (360° horizontal, 75° vertical, cylindrical, 12m range) ---
Lidar {
translation 0 0 %<= lidZ >%
rotation 0 0 -1 -1.5707953071795862
name "lidar"
horizontalResolution 1024
fieldOfView 6.2832
verticalFieldOfView 1.309
numberOfLayers 60
near 0.05
minRange %<= wR >%
maxRange 12.0
noise 0.005
projection "cylindrical"
}
# --- Fisheye Cameras (back-to-back, 170° each, spherical projection) ---
Camera {
translation 0 0 %<= fishZ >%
rotation 0 0 1 0
children [
Shape {
appearance Appearance { material Material {} }
geometry Box { size 0.05 0.05 0.05 }
}
]
name "fisheye_front"
fieldOfView 2.96706
width 640
height 480
projection "spherical"
}
Camera {
translation 0 0 %<= fishZ >%
rotation 0 0 1 3.14159
children [
Shape {
appearance Appearance { material Material {} }
geometry Box { size 0.05 0.05 0.05 }
}
]
name "fisheye_rear"
fieldOfView 2.96706
width 640
height 480
projection "spherical"
}
# --- GPS (3D position + velocity) ---
GPS {
rotation 0 0 1 1.5708
name "gps"
}
# --- InertialUnit (roll/pitch/yaw) ---
InertialUnit {
rotation 0 0 1 1.5708
name "imu"
}
# --- Compass (bearing angle) ---
Compass {
rotation 0 0 1 1.5708
name "compass"
}
]
}
]
# --- Wheel shapes (VehicleWheel PROTO, sized for RC tires) ---
# tireRadius=0.08, thickness=0.09, rimRadius=0.05
wheelFrontRight VehicleWheel {
name "front right wheel"
thickness %<= wW >%
tireRadius %<= wR >%
curvatureFactor 0.2
rimRadius 0.05
rimBeamWidth 0.03
rimBeamThickness %<= wW >%
rimBeamOffset 0
centralInnerRadius 0.01
centralOuterRadius 0.02
wheelSide FALSE
physics Physics { density 100 }
}
wheelFrontLeft VehicleWheel {
name "front left wheel"
thickness %<= wW >%
tireRadius %<= wR >%
curvatureFactor 0.2
rimRadius 0.05
rimBeamWidth 0.03
rimBeamThickness %<= wW >%
rimBeamOffset 0
centralInnerRadius 0.01
centralOuterRadius 0.02
wheelSide TRUE
physics Physics { density 100 }
}
wheelRearRight VehicleWheel {
name "rear right wheel"
thickness %<= wW >%
tireRadius %<= wR >%
curvatureFactor 0.2
rimRadius 0.05
rimBeamWidth 0.03
rimBeamThickness %<= wW >%
rimBeamOffset 0
centralInnerRadius 0.01
centralOuterRadius 0.02
wheelSide FALSE
physics Physics { density 100 }
}
wheelRearLeft VehicleWheel {
name "rear left wheel"
thickness %<= wW >%
tireRadius %<= wR >%
curvatureFactor 0.2
rimRadius 0.05
rimBeamWidth 0.03
rimBeamThickness %<= wW >%
rimBeamOffset 0
centralInnerRadius 0.01
centralOuterRadius 0.02
wheelSide TRUE
physics Physics { density 100 }
}
}
}
rover_controller.py:
#!/usr/bin/env python3
"""
rover_controller.py — Per-car controller using Webots Driver API.
Inherits from vehicle.Driver (official Webots API).
setSteeringAngle() auto-computes Ackermann geometry.
setThrottle() / setBrakeIntensity() for drive control.
Receives commands from Supervisor via Receiver (channel 1).
Reports collision events via TouchSensor (force-3d).
Command protocol (string):
"{NAME}:{throttle},{steering},{brake}"
throttle ∈ [-1, 1] → negative → gear=-1 (reverse)
steering ∈ [-1, 1] → mapped to ±MAX_STEER rad
brake ∈ {0, 1}
"{NAME}:SENSOR" → print all sensor data
"{NAME}:SAVE" → save camera images
Resetting (teleport) is handled by the Supervisor, not this controller.
Only the Supervisor knows the correct spawn/checkpoint position.
"""
import math
import os
from vehicle import Driver
# ------------------------------------------------------------------ config
MAX_STEER = 0.5236 # rad (30°) — commanded steering angle. Proto maxSteeringAngle=0.7
# is the joint limit, larger to accommodate inner-wheel Ackermann (~0.672 rad).
COLLISION_THRESHOLD = 0.5 # N — below this = ignore
COLLISION_PART = "touchSensorChassis"
COLLISION_LABEL = "Chassis"
# ------------------------------------------------------------------ helpers
def clamp(v, lo, hi):
return max(lo, min(hi, v))
# ------------------------------------------------------------------ init
class RoverController(Driver):
def __init__(self):
super().__init__()
self._timestep = int(self.getBasicTimeStep())
self._robot_name = self.getName()
# Receiver (commands from Supervisor)
self._receiver = self.getDevice("receiver")
self._receiver.enable(self._timestep)
# Sensors
self._camera = self.getDevice("camera")
self._fisheye_f = self.getDevice("fisheye_front")
self._fisheye_r = self.getDevice("fisheye_rear")
self._lidar = self.getDevice("lidar")
self._gps = self.getDevice("gps")
self._imu = self.getDevice("imu")
self._compass = self.getDevice("compass")
self._ts_chassis = self.getDevice(COLLISION_PART)
sensor_list = [
self._camera, self._fisheye_f, self._fisheye_r,
self._lidar, self._gps, self._imu, self._compass,
self._ts_chassis,
]
for dev in sensor_list:
if dev:
dev.enable(self._timestep)
if self._lidar:
self._lidar.enablePointCloud()
# State
self._img_idx = 0
self._throttle = 0.0
self._steering = 0.0
self._braking = False
# Collision callback (replaceable for RL)
self.on_collision = self._default_on_collision
# ------------------------------------------------------------------
# Collision
# ------------------------------------------------------------------
@staticmethod
def _default_on_collision(part_label, force_vec, magnitude):
fx, fy, fz = force_vec
print(
f"[Rover] ! COLLISION on [{part_label}] | "
f"Force={magnitude:.2f}N | Dir=({fx:+.2f},{fy:+.2f},{fz:+.2f})"
)
def check_collision(self):
fv = self._ts_chassis.getValues()
if fv is None:
return
fx, fy, fz = fv[0], fv[1], fv[2]
mag = math.hypot(fx, fy, fz)
if mag > COLLISION_THRESHOLD:
self.on_collision(COLLISION_LABEL, (fx, fy, fz), mag)
# ------------------------------------------------------------------
# Control — Driver API handles Ackermann + motor management internally
# ------------------------------------------------------------------
def apply_control(self, throttle, steering, brake):
"""
Apply throttle, steering, brake using Driver API.
- steering: [-1, 1] mapped to ±MAX_STEER rad
- throttle: [-1, 1], negative → gear=-1 + abs throttle
- brake: [0, 1]
"""
# Steering
angle = clamp(steering, -1.0, 1.0) * MAX_STEER
self.setSteeringAngle(angle)
# Gear + Throttle
if throttle < -0.01:
self.setGear(-1)
self.setThrottle(clamp(abs(throttle), 0.0, 1.0))
elif throttle > 0.01:
self.setGear(1)
self.setThrottle(clamp(throttle, 0.0, 1.0))
else:
self.setThrottle(0.0)
# Brake (independent of throttle)
self.setBrakeIntensity(1.0 if brake else 0.0)
self._throttle = throttle
self._steering = steering
self._braking = brake
# ------------------------------------------------------------------
# Sensor info (for debugging)
# ------------------------------------------------------------------
def print_sensor_info(self):
p = f"[{self._robot_name}]"
gpos = self._gps.getValues()
print(
f"{p} --- GPS --- "
f"pos=({gpos[0]:.3f},{gpos[1]:.3f},{gpos[2]:.3f}), "
f"speed={self._gps.getSpeed():.2f}m/s"
)
rpy = self._imu.getRollPitchYaw()
print(
f"{p} --- IMU --- "
f"rpy=({math.degrees(rpy[0]):.1f},{math.degrees(rpy[1]):.1f},{math.degrees(rpy[2]):.1f})deg"
)
cv = self._compass.getValues()
bearing = (math.degrees(math.atan2(cv[1], cv[0])) + 360) % 360
print(f"{p} --- Compass --- bearing={bearing:.1f}deg")
if self._camera:
print(
f"{p} --- Camera --- "
f"{self._camera.getWidth()}x{self._camera.getHeight()}, "
f"FOV={math.degrees(self._camera.getFov()):.1f}deg"
)
if self._fisheye_f:
print(
f"{p} --- Fisheye Front --- "
f"{self._fisheye_f.getWidth()}x{self._fisheye_f.getHeight()}, "
f"FOV={math.degrees(self._fisheye_f.getFov()):.1f}deg"
)
if self._fisheye_r:
print(
f"{p} --- Fisheye Rear --- "
f"{self._fisheye_r.getWidth()}x{self._fisheye_r.getHeight()}, "
f"FOV={math.degrees(self._fisheye_r.getFov()):.1f}deg"
)
if self._lidar:
print(
f"{p} --- Lidar --- "
f"{self._lidar.getNumberOfLayers()}L×"
f"{self._lidar.getHorizontalResolution()}pts, "
f"VertFOV={math.degrees(self._lidar.getVerticalFov()):.1f}deg, "
f"Range=[{self._lidar.getMinRange()},{self._lidar.getMaxRange()}]m"
)
print(
f"{p} --- Driver --- "
f"speed={self.getCurrentSpeed():.2f}km/h, "
f"gear={self.getGear()}, "
f"steering={math.degrees(self.getSteeringAngle()):.1f}deg"
)
def save_images(self):
d = os.path.dirname(os.path.abspath(__file__))
cam_tags = [
(self._camera, "cam_front"),
(self._fisheye_f, "fisheye_front"),
(self._fisheye_r, "fisheye_rear"),
]
for cam, tag in cam_tags:
if cam is None:
continue
fp = os.path.join(d, f"{self._robot_name}_{tag}_{self._img_idx:04d}.png")
cam.saveImage(fp, 80)
print(f"[{self._robot_name}] Saved: {fp}")
self._img_idx += 1
# ------------------------------------------------------------------
# Command processing
# ------------------------------------------------------------------
def process_command(self, msg):
if ":" not in msg:
return
target, data = msg.split(":", 1)
if target != self._robot_name:
return
if data == "SENSOR":
self.print_sensor_info()
elif data == "SAVE":
self.save_images()
else:
try:
parts = data.split(",")
thr = float(parts[0])
steer = float(parts[1])
brk = int(parts[2]) > 0 if len(parts) > 2 else False
self.apply_control(thr, steer, brk)
except (ValueError, IndexError):
pass
# ------------------------------------------------------------------
# Main loop
# ------------------------------------------------------------------
def run(self):
print(
f"[{self._robot_name}] Rover ready (Driver API). "
f"Sensors: camera(front,60°)+lidar(60L/360°)+"
f"fisheye×2(170°,spherical)+gps+imu+compass+collision(chassis)"
)
print(
f"[{self._robot_name}] Control: "
f"setSteeringAngle(±{math.degrees(MAX_STEER):.0f}° auto-Ackermann) + "
f"setThrottle(0–1) + setBrakeIntensity(0–1)"
)
while self.step() != -1:
# Process commands from Supervisor
while self._receiver.getQueueLength() > 0:
self.process_command(self._receiver.getString())
self._receiver.nextPacket()
# Per-frame checks
self.check_collision()
# ======================================================================
if __name__ == "__main__":
rover = RoverController()
rover.run()
Describe the Bug
A clear and concise description of what the bug is.
“When creating a rover.proto using the Car proto, after frequent left and right turns, the two front wheels gradually become misaligned. I tried adjusting many parameters (e.g., maxSteeringTorque, wheelsDampingConstant, suspensionFrontSpringConstant, suspensionFrontDampingConstant .etc), but it didn’t help.
Also, when setting coulombFriction in contactProperties: if the value is relatively low, the rover moves very slowly and cannot reach the max speed. If coulombFriction is relatively high, the speed improves, but once turning or braking, the rover spins out of control. How can I prevent the rover from spinning out during turns or braking while still allowing it to reach a relatively high speed (i.e., the set max speed)?”
track.wbt:
Rover.proto:
rover_controller.py: