fix(simulation): tune physical UGV braking

This commit is contained in:
DCCONSTRUCTIONS
2026-08-29 11:26:01 +03:00
parent af1e530162
commit d9ec8c9cef
2 changed files with 31 additions and 24 deletions
@@ -24,7 +24,9 @@ const PHYSICS_PROXY_ERROR = 0.0003;
const PHYSICS_PROXY_MAX_TRIANGLES = 240_000; const PHYSICS_PROXY_MAX_TRIANGLES = 240_000;
const SERVICE_BRAKE_DECELERATION_MPS2 = 1.8; const SERVICE_BRAKE_DECELERATION_MPS2 = 1.8;
const COAST_DECELERATION_MPS2 = 0.18; const COAST_DECELERATION_MPS2 = 0.18;
const BRAKE_ATTITUDE_DAMPING = 6; const PARKING_BRAKE_HOLD_DECELERATION_MPS2 = 6;
const PARKING_BRAKE_ENGAGE_SPEED_MPS = 0.08;
const TYRE_FRICTION_SLIP = 8.5;
const DEFAULT_ORBIT_PITCH = 0.48; const DEFAULT_ORBIT_PITCH = 0.48;
const CAMERA_RETURN_DELAY_SECONDS = 1.2; const CAMERA_RETURN_DELAY_SECONDS = 1.2;
const CAMERA_RETURN_DURATION_SECONDS = 2; const CAMERA_RETURN_DURATION_SECONDS = 2;
@@ -311,6 +313,7 @@ export class SimulationUgvController {
const maxSpeed = this.settings.maxSpeedMetersPerSecond; const maxSpeed = this.settings.maxSpeedMetersPerSecond;
const maxTurnRate = this.settings.maxTurnRateDegrees * Math.PI / 180; const maxTurnRate = this.settings.maxTurnRateDegrees * Math.PI / 180;
const pureTurn = !braking && forwardInput === 0 && turnInput !== 0; const pureTurn = !braking && forwardInput === 0 && turnInput !== 0;
const holding = !braking && forwardInput === 0 && turnInput === 0;
const desiredSpeed = forwardInput * maxSpeed; const desiredSpeed = forwardInput * maxSpeed;
const speedError = desiredSpeed - speedMetersPerSecond; const speedError = desiredSpeed - speedMetersPerSecond;
const speedResponseRange = Math.max(0.35, maxSpeed * 0.2); const speedResponseRange = Math.max(0.35, maxSpeed * 0.2);
@@ -325,13 +328,22 @@ export class SimulationUgvController {
const leftCommand = clamp(forwardCommand - turnCommand, -1, 1); const leftCommand = clamp(forwardCommand - turnCommand, -1, 1);
const rightCommand = clamp(forwardCommand + turnCommand, -1, 1); const rightCommand = clamp(forwardCommand + turnCommand, -1, 1);
const engineForce = pureTurn ? pivotForcePerWheel : driveForcePerWheel; const engineForce = pureTurn ? pivotForcePerWheel : driveForcePerWheel;
const brakeDeceleration = braking
? SERVICE_BRAKE_DECELERATION_MPS2
: holding
? Math.abs(speedMetersPerSecond) <= PARKING_BRAKE_ENGAGE_SPEED_MPS
? PARKING_BRAKE_HOLD_DECELERATION_MPS2
: COAST_DECELERATION_MPS2
: 0;
const wheelBrakeForce = this.settings.massKg * brakeDeceleration
/ Math.max(1, this.wheelDefinitions.length);
for (let index = 0; index < this.wheelDefinitions.length; index += 1) { for (let index = 0; index < this.wheelDefinitions.length; index += 1) {
const definition = this.wheelDefinitions[index]; const definition = this.wheelDefinitions[index];
const command = definition.left ? leftCommand : rightCommand; const command = definition.left ? leftCommand : rightCommand;
this.vehicle.setSteeringValue(0, index); this.vehicle.setSteeringValue(0, index);
this.vehicle.applyEngineForce(command * engineForce, index); this.vehicle.applyEngineForce(command * engineForce, index);
this.vehicle.setBrake(0, index); this.vehicle.setBrake(wheelBrakeForce, index);
this.vehicle.updateWheelTransform(index, true); this.vehicle.updateWheelTransform(index, true);
const transform = this.vehicle.getWheelTransformWS(index); const transform = this.vehicle.getWheelTransformWS(index);
const position = transform.getOrigin(); const position = transform.getOrigin();
@@ -345,17 +357,6 @@ export class SimulationUgvController {
let nextLinearX = linearVelocity.x; let nextLinearX = linearVelocity.x;
let nextLinearZ = linearVelocity.z; let nextLinearZ = linearVelocity.z;
let horizontalSpeed = Math.hypot(nextLinearX, nextLinearZ); let horizontalSpeed = Math.hypot(nextLinearX, nextLinearZ);
const coasting = !braking && forwardInput === 0 && turnInput === 0;
if ((braking || coasting) && horizontalSpeed > 0.001) {
const deceleration = braking
? SERVICE_BRAKE_DECELERATION_MPS2
: COAST_DECELERATION_MPS2;
const nextSpeed = Math.max(0, horizontalSpeed - deceleration * Math.max(0, deltaSeconds));
const scale = nextSpeed / horizontalSpeed;
nextLinearX *= scale;
nextLinearZ *= scale;
horizontalSpeed = nextSpeed;
}
if (pureTurn && horizontalSpeed > 0.001) { if (pureTurn && horizontalSpeed > 0.001) {
const pivotDamping = Math.exp(-Math.max(0, deltaSeconds) * 8); const pivotDamping = Math.exp(-Math.max(0, deltaSeconds) * 8);
nextLinearX *= pivotDamping; nextLinearX *= pivotDamping;
@@ -372,9 +373,6 @@ export class SimulationUgvController {
rigidbody.linearVelocity = this.limitedLinearVelocity; rigidbody.linearVelocity = this.limitedLinearVelocity;
} }
const angularVelocity = rigidbody.angularVelocity; const angularVelocity = rigidbody.angularVelocity;
const attitudeDamping = braking
? Math.exp(-Math.max(0, deltaSeconds) * BRAKE_ATTITUDE_DAMPING)
: 1;
let nextAngularY = angularVelocity.y; let nextAngularY = angularVelocity.y;
if (pureTurn) { if (pureTurn) {
const desiredYawRate = -turnInput * maxTurnRate; const desiredYawRate = -turnInput * maxTurnRate;
@@ -387,11 +385,11 @@ export class SimulationUgvController {
} else if (Math.abs(angularVelocity.y) > maxTurnRate) { } else if (Math.abs(angularVelocity.y) > maxTurnRate) {
nextAngularY = Math.sign(angularVelocity.y) * maxTurnRate; nextAngularY = Math.sign(angularVelocity.y) * maxTurnRate;
} }
if (attitudeDamping !== 1 || nextAngularY !== angularVelocity.y) { if (nextAngularY !== angularVelocity.y) {
this.limitedAngularVelocity.set( this.limitedAngularVelocity.set(
angularVelocity.x * attitudeDamping, angularVelocity.x,
nextAngularY, nextAngularY,
angularVelocity.z * attitudeDamping, angularVelocity.z,
); );
rigidbody.angularVelocity = this.limitedAngularVelocity; rigidbody.angularVelocity = this.limitedAngularVelocity;
} }
@@ -464,8 +462,9 @@ export class SimulationUgvController {
type: "dynamic", type: "dynamic",
mass: this.settings.massKg, mass: this.settings.massKg,
friction: 0.85, friction: 0.85,
linearDamping: 0.08, rollingFriction: 0.12,
angularDamping: 0.45, linearDamping: 0.12,
angularDamping: 0.6,
}); });
this.chassisMaterial = createMaterial(readThemeAccent(), new Color(0.03, 0.04, 0.05)); this.chassisMaterial = createMaterial(readThemeAccent(), new Color(0.03, 0.04, 0.05));
@@ -559,7 +558,7 @@ export class SimulationUgvController {
wheel.set_m_suspensionStiffness(24); wheel.set_m_suspensionStiffness(24);
wheel.set_m_wheelsDampingRelaxation(3.2); wheel.set_m_wheelsDampingRelaxation(3.2);
wheel.set_m_wheelsDampingCompression(4.8); wheel.set_m_wheelsDampingCompression(4.8);
wheel.set_m_frictionSlip(5.5); wheel.set_m_frictionSlip(TYRE_FRICTION_SLIP);
wheel.set_m_rollInfluence(0.08); wheel.set_m_rollInfluence(0.08);
} }
this.ammo.destroy(axle); this.ammo.destroy(axle);
@@ -179,8 +179,16 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
assert.match(ugv, /desiredSpeed = forwardInput \* maxSpeed/); assert.match(ugv, /desiredSpeed = forwardInput \* maxSpeed/);
assert.match(ugv, /maximumAcceleration = clamp\(1\.4 \+ maxSpeed \* 0\.35, 1\.8, 4\.2\)/); assert.match(ugv, /maximumAcceleration = clamp\(1\.4 \+ maxSpeed \* 0\.35, 1\.8, 4\.2\)/);
assert.match(ugv, /SERVICE_BRAKE_DECELERATION_MPS2 = 1\.8/); assert.match(ugv, /SERVICE_BRAKE_DECELERATION_MPS2 = 1\.8/);
assert.match(ugv, /horizontalSpeed - deceleration \* Math\.max\(0, deltaSeconds\)/); assert.match(ugv, /PARKING_BRAKE_HOLD_DECELERATION_MPS2 = 6/);
assert.match(ugv, /this\.vehicle\.setBrake\(0, index\)/); assert.match(ugv, /PARKING_BRAKE_ENGAGE_SPEED_MPS = 0\.08/);
assert.match(ugv, /TYRE_FRICTION_SLIP = 8\.5/);
assert.match(ugv, /holding = !braking && forwardInput === 0 && turnInput === 0/);
assert.match(ugv, /this\.settings\.massKg \* brakeDeceleration/);
assert.match(ugv, /this\.vehicle\.setBrake\(wheelBrakeForce, index\)/);
assert.doesNotMatch(ugv, /horizontalSpeed - deceleration \* Math\.max\(0, deltaSeconds\)/);
assert.match(ugv, /rollingFriction: 0\.12/);
assert.match(ugv, /angularDamping: 0\.6/);
assert.match(ugv, /wheel\.set_m_frictionSlip\(TYRE_FRICTION_SLIP\)/);
assert.doesNotMatch(ugv, /massKg \* 3/); assert.doesNotMatch(ugv, /massKg \* 3/);
assert.match(ugv, /pureTurn = !braking && forwardInput === 0 && turnInput !== 0/); assert.match(ugv, /pureTurn = !braking && forwardInput === 0 && turnInput !== 0/);
assert.match(ugv, /desiredYawRate = -turnInput \* maxTurnRate/); assert.match(ugv, /desiredYawRate = -turnInput \* maxTurnRate/);