Vehicles, impact damage, deformable meshes, detachable parts
These are engine features with components, commands, schemas and deterministic tests, usable by any game. The vehicle part is the driving model (like Chaos Vehicles: wheels, tires, engine, gearbox, telemetry).
Impact damage, DeformableMesh and DetachablePart are general: they work on any dynamic body (a crate, an armoured turret, a door, a car) and see only contact forces; what damage means for a game (how much a wrecked engine slows a car,
what the HUD shows, repair in the pits) is data in DamageDesc (engine_min_power, wheel_grip_loss, suspension_loss, steer_pull, thresholds) or a rule of the game reading the telemetry and the damage state. See Impact damage on any body below.
| Layer | Where | What |
|---|---|---|
| Driving model | sparklight-physics-fx vehicle.rs |
suspension, Pacejka tires, engine, clutch, gearbox, differentials, aids, input shaping, telemetry |
| Damage model | sparklight-physics-fx damage.rs |
hits from contact events, dents, breakable parts, body health; on a vehicle also engine, wheel and suspension damage that changes the handling; repair. VehicleDamage::new (vehicle) or on_body (any dynamic body) |
| Deformable mesh | sparklight-physics-fx deform.rs, sparklight-3d dynamic_mesh.rs |
vertex displacement with a clamp, smoothing and recomputed normals; in-place GPU vertex update |
| Components | sparklight-scene3d |
Vehicle, VehicleDamage, DeformableMesh, DetachablePart (space3d.component_schema <name> gives the schema) |
| Commands | sparklight-control |
space3d.vehicle.input, .telemetry, .damage, .repair, .tune, .reset (each is an MCP tool space3d_vehicle_*); space3d.damage.hit, space3d.damage.repair for any body |
Units: metres, kilograms, seconds, radians, newtons. Y is up, a car faces +Z. steer = +1 turns right (clockwise from above, toward -X);
the wheel angle in telemetry is positive for a right turn.
Samples: samples/space3d/car-crash.space3d.json (floor, wall, car with damage, mesh and three parts) and
samples/space3d/scenarios/car-crash.scenario3d.json (drive, crash, assert, goldens, snapshot restore, repair, determinism);
spark test samples/space3d/car-crash.test.json --project ..
Quick start
Section titled “Quick start”// A car that crumples: the object carries the vehicle, the damage model and the deformable picture of its body.{"command": "space3d.spawn", "params": {"name": "Car", "components": { "Transform3D": {"position": [0, 0.93, 0]}, "MeshRenderer": {"model": "builtin:car_body", "base_color": [0.8, 0.1, 0.1, 1]}, // or your glTF "Vehicle": {"preset": "sedan"}, "VehicleDamage": {}, "DeformableMesh": {}}}}// A bumper: an object where it sits on the car; it rides on the chassis until a hit tears it off.{"command": "space3d.spawn", "params": {"name": "BumperFront", "components": { "Transform3D": {"position": [0, 0.63, 2.16], "scale": [1.7, 0.3, 0.24]}, "MeshRenderer": {"model": "builtin:cube"}, "DetachablePart": {"vehicle": "Car", "half_extents": [0.85, 0.15, 0.12]}}}}{"command": "space3d.vehicle.input", "params": {"entity": "Car", "throttle": 1, "steer": 0.2, "analog": true}}{"command": "space3d.step", "params": {"frames": 300}}{"command": "space3d.vehicle.telemetry", "params": {"entity": "Car"}} // speed, rpm, gear, wheels[], skid, surface, damage{"command": "space3d.vehicle.damage", "params": {"entity": "Car", "speed": 9}} // a hit by hand{"command": "space3d.vehicle.repair", "params": {"entity": "Car"}}Driving model
Section titled “Driving model”Vehicle::spawn(&mut world, VehicleDesc, pose), set_input, step(&mut world) before PhysicsWorld::step, then sync_after_step(&world) (Space3D does
it): step writes WheelState::center from the pose the step started from, and sync_after_step moves it to the pose the chassis ended on. Without the sync
the centre is one tick behind the chassis (speed / tick rate metres: 0.5 m at 30 m/s and 60 Hz). WheelState::local_center is the same centre in the chassis
frame (the anchor, suspension_rest - compression down); a game that draws the chassis on an interpolated pose draws the wheel at that pose applied to
local_center. contact_point is where the ray hit the ground when the step ran; the sync leaves it. The component is a preset plus a
merge patch of the description: {"preset": "rally", "overrides": {"tires": {"lateral": {"d": 1.1}}}}. Presets: sedan (1200 kg, rear drive,
ABS and traction control on), sport (1150 kg, rear drive, limited slip, sticky tires and downforce: holds about 1.3 g at 20 m/s, 0 to 100 km/h in under 6 s, a step steer at 30 m/s settles with 1 percent overshoot) and rally (1300 kg, all-wheel drive, limited-slip differentials, stiffer, traction control off). Every field has a
default, so a description can be partial. space3d.vehicle.tune patches a live car (the patch also goes into the component’s overrides);
the wheels, chassis size, mass and centre of mass cannot change on a live car (E_FX_VEHICLE_DESC).
Suspension and chassis
Section titled “Suspension and chassis”The chassis collides as one box (chassis_half_extents, its middle com_drop below the chassis origin). Without center_of_mass that
box’s middle is the centre of mass, so a box tall enough to match the visible body puts the weight high and the car rolls at full lock.
center_of_mass: [x, y, z] (chassis frame) moves the mass there and keeps the box for collisions (the inertia stays the box’s, applied as
rapier mass properties): measured with a box from 0.18 below to 0.62 above the origin, full lock at 25 m/s rolls to up.y -0.04 with the
box middle and stays at 0.999 with the centre at y = -0.30 (crates/sparklight-physics-fx/tests/vehicle_com.rs). It is part of the
layout: it survives a rebuild and cannot change on a live car. Plain bodies get the same through ColliderDesc::mass_properties
(MassPropertiesDesc; PhysicsWorld::center_of_mass reads the result).
One ray per wheel from the anchor, length suspension_rest + droop + wheel_radius, where droop is suspension_droop (the most the suspension extends below
rest; finite and 0 or more, tunable on a live car) or, when it is unset, suspension_travel. The static wheel already sits sag = m g / (4 k) below rest, so
the lowest wheel hangs sag + droop below its static position at full extension. suspension_travel is the compression room above rest before the bump stop and
stays the same whatever the droop: a road car with 10-15 cm of droop below the static position sets suspension_droop to that less the sag and keeps its
travel. Per wheel: spring (spring_stiffness), damper
with separate bump and rebound rates (damper times damper_bump / damper_rebound), a bump stop (bump_stop_stiffness beyond
suspension_travel) and an anti-roll bar between the wheels of one axle (anti_roll). Loads act on the chassis as impulses along its up axis at the contact
points, so weight transfer, squat, dive and roll are real. Measured on this machine: static compression equals weight / (4 k) to 0.1 percent, a
1 m drop settles without a bounce (largest compression 0.218 m of 0.25 m travel), a stiff anti-roll bar halves body roll in a 16 m/s turn
(3.9 degrees to 1.8).
Pacejka magic formula per axis (tires.longitudinal, tires.lateral: b stiffness, c shape, d peak as a multiple of mu times load, e curvature):
force / (mu * load) = d sin(c atan(b x - e (b x - atan(b x)))), x = slip ratio or slip angle (radians). Defaults peak at slip ratio 0.135
and slip angle 0.125 rad (7 degrees), fall to 0.70 and 0.77 of the peak at full slip. load_sensitivity lowers grip per newton on a loaded
wheel, a friction ellipse combines the two forces, the lateral slip angle lags over relaxation_length metres (soft at low speed), rolling_resistance is a coefficient on the load. mu = tire_friction x the ground
collider’s friction x the surface grip x the damage factor. The wheel’s speed is integrated implicitly with the secant of the tire curve, so a
locked or spinning wheel is stable at 60 Hz, and a force that slows the car cannot take more speed than the wheel’s share of the car has (a parked car does not jitter).
Surfaces: the Body component’s surface field ("surface": "grass") or, in Rust, PhysicsWorld::set_collider_surface(collider, id) tags a collider (0 asphalt, 1 concrete, 2 grass, 3 dirt, 4 sand, 5 gravel, 6 snow,
7 ice, 8 wet; SURFACE_NAMES); wheels take the grip and rolling resistance of the surface under them (surface_grip) and report its name.
Drivetrain
Section titled “Drivetrain”Engine: torque curve (engine.torque_curve [[rpm, Nm], ...] or the parametric one from peak_torque, idle_rpm, peak_rpm, redline_rpm), flywheel
inertia, engine_brake off throttle, an idle governor, a rev limiter with hysteresis. Clutch: its torque capacity is clutch_capacity times the peak torque; the
engine, the clutch and the driven wheels are solved together as one implicit system each step, so launches, shifts and engine braking come out of
one equation without mode switches. An automatic clutch feathers a launch (it transmits no more than the driven tires can use until the car rolls) and opens for
the first part of a shift (shift_time). Gearbox: automatic (shift points follow the throttle, kick-down, no hunting: 4 shifts in a 35 s
full-throttle run through 5 gears) or manual (VehicleInput::shift +1/-1 and gear -1 reverse, 0 neutral, 1..N are one-shot; engine.auto_clutch: false
makes VehicleInput::clutch work the pedal). Reverse: a negative throttle, or the brake held at a standstill (aids.reverse_on_brake, arcade pedals).
An automatic car standing still and not asked to move holds itself with a light brake.
Differentials per driven axle (differential: open, limited_slip with preload, power_lock, coast_lock, or locked) and center_split
(rear share) for all-wheel drive. Measured: from a split-mu start (ice under one side) an open differential reaches 2.3 m/s in 5 s, limited slip 8.0 m/s; on gravel the
rally car reaches 14.4 m/s rear-driven and 22.6 m/s with all-wheel drive.
Brakes, handbrake, aids, aerodynamics
Section titled “Brakes, handbrake, aids, aerodynamics”brake_torque with brake_bias (front share). The handbrake locks the handbrake wheels and multiplies their side grip by handbrake_grip (drifts: a handbrake turn at
20 m/s with half steering reaches a drift angle of about 88 degrees in the test). aids.abs: the brake never asks for more than the tire can hold, so a wheel stays on the rising part of its
curve: braking from 28 m/s takes 40.8 m with ABS and no locked wheel, 51.6 m without it (168 steps with a locked wheel). aids.tcs: cuts torque when a driven wheel’s slip speed
exceeds what the tire can use. aids.drift_assist (0 to 1) steers the front wheels toward the velocity in a slide. drag and downforce (coefficient times speed squared),
downforce_front splits the downforce between the axles.
Input feel (keyboard and gamepad)
Section titled “Input feel (keyboard and gamepad)”VehicleInput {throttle, brake, steer, handbrake, clutch, shift, gear, analog}. analog: false (keys): pedals ramp (aids.throttle_rise, throttle_fall,
brake_rise, brake_fall) and the steering wheel moves at aids.steer_rate (fraction of lock per second) and returns at steer_return_rate; analog: true (stick,
triggers) follows the value almost directly (steer_rate_analog). The steering rate falls with speed (steer_rate_speed_falloff), and the lock narrows from steer_lock
to steer_lock_at_speed at steer_speed (31.5 degrees at a standstill, 11.2 at 30 m/s). Steady-state yaw rate follows the bicycle model within 2 percent at 7.5 m/s.
Telemetry
Section titled “Telemetry”vehicle.telemetry() / space3d.vehicle.telemetry (JSON): speed, forward_speed, speed_kmh, rpm, rpm_norm, gear, gear_count, clutch, throttle, brake,
steer, steer_angle, handbrake, engine_torque, shifting, rev_limiter, abs_active, tcs_active, airborne, skid (strongest wheel, 0 to 1), skid_mean,
surface, drift_angle, longitudinal_g, lateral_g, yaw_rate, power_scale, wheels[] (in_contact, load, slip_ratio, slip_angle, skid, surface, compression,
suspension_velocity, rpm, steer_angle, position), plus position, heading, frame and the damage state in the command. Audio reads rpm_norm, throttle, shifting,
rev_limiter; tyre smoke and skid marks read wheels[].skid and surface; a HUD reads speed_kmh, gear, rpm.
Damage model
Section titled “Damage model”VehicleDamage::new(&mut world, &vehicle, DamageDesc); per step after world.step(): damage.step(&mut world, &mut vehicle, &events) (the component does this).
- Hits come from the contact-force events of the car’s colliders (
PhysicsEvent::ContactForcenow carries the contactnormal). A hit is the speed changedv = force * dt / mass; hits withdv <= speed_threshold(default 2 m/s) do nothing, so scrapes, cones and gentle bumps are free. Above it,speed = dv - thresholddrives everything below. - Dents: each hit is recorded as a
DeformImpact(point and push direction in the car’s space, depth =depth_per_speedx speed, radius). The record is the state: the state hash covers it, snapshots keep it, and a mesh that is built later (or on another machine) is rebuilt from it identically. Measured: 4.5 / 9 / 16 m/s into a wall record 0.14 / 0.42 / 0.85 m of depth and leave 91 / 72 / 43 percent of the body health. - Systems: body, engine, and per wheel tire and suspension are health values from 1 to 0. The engine loses health from hits in the engine bay (
engine_zone_*), wheels and suspension from hits withinwheel_reachof their anchor. They setVehicleModifiers: engine power down toengine_min_power, grip down bywheel_grip_loss, spring and damper down bysuspension_loss, weaker brakes, a steering pull toward the side of a bent front suspension (steer_pull), more drag. Measured: after four 10 m/s hits on the engine bay the car reaches 6.6 m/s in 6 s instead of 23.8; a bent front wheel pulls a car driving straight 1 m sideways in 5 s. - Parts (
PartDescin Rust,DetachablePartobjects in a scene): a box collider on the chassis held by a breakable joint. The joint is a force and impulse threshold on the contact events of that collider, not a rapier impulse joint: a stiff joint between a 12 kg part and a 1200 kg chassis jitters at 60 Hz and costs a body per part, the threshold does neither, and it is exact (the part breaks in the step the hit happens). A contact force abovebreak_forceon the part (a 1200 kg car stopping dead from 4 m/s in one step is 290 kN), or hits whose attributed impulse adds up tobreak_impulse, tear it off: the collider leaves the chassis and a dynamic debris body takes over with the car’s velocity at that point plusdetach_speed, a deterministic tumble, collision layerDEBRIS_LAYER(wheel rays ignore it). Debris lies around fordebris_lifetime(14 s), shrinks overdebris_fade, then its body is removed (eventpart_expired). Hits on the body load the parts withinreach. A part that is loaded shows it: its joint droops (loose_tilt) and it moves back with the dents near it (dent_at). A 3.2 m/s bump tears nothing off, 6 m/s tears the front bumper, three 4 m/s shoves add up to a broken joint. - Repair (
repair,space3d.vehicle.repair): systems to 1, record cleared (the mesh goes back to the exact original arrays), parts re-attached where they were attached, modifiers neutral.
DamageDesc fields (all defaulted; DamageDesc::for_vehicle(&desc) fits the crumple regions and the engine bay to the chassis): deform (below), speed_threshold, depth_per_speed, impact_radius, damage_per_speed,
engine_zone_center, engine_zone_half, engine_min_power, wheel_grip_loss, suspension_loss, steer_pull, wheel_reach, debris_lifetime, debris_fade, parts.
The component’s overrides is a merge patch of it (without parts).
Deformable meshes
Section titled “Deformable meshes”DeformMesh::new(positions, normals, triangles, DeformDesc): vertices at the same place are welded into one group (so the mesh never cracks open at flat-shaded corners or UV seams). apply_impact moves every vertex
within the radius along the push direction (blended with the inward surface normal by inward_blend), scaled by a smooth falloff (1 - (d/r)^2)^2, by how much the surface faces the impact, by the
crumple strength of its region (regions: boxes with strength and an optional max_deform), and by a deterministic crinkle noise (crinkle, crinkle_scale); the total is clamped with a soft
limit (max_deform x tanh(len / max_deform), so it is never exceeded: 100 violent hits stay within 0.4992 m of a 0.5 m limit), then smoothed over the mesh (smoothing, smooth_iterations). update(dt) eases the vertices
toward their targets (a dent forms in about 35 steps, settle_rate), recomputes the normals of what moved from the deformed triangles and bumps version(); when nothing moves it does nothing (3 ns). Cost on a 3,602 vertex body: build 1.7 ms once,
36.7 microseconds per impact, 72 microseconds per settling step, 115 kB written to the GPU per changed frame.
builtin:car_body (also car_body_mesh(half_extents)) is a 3,602 vertex sedan body for cars that need a shape before a real model exists. A real model works the same: the DeformableMesh component deforms every mesh of the
model (instance transforms baked in; skinned meshes are drawn as they are). The model’s origin must be the chassis origin. Parts are separate objects: the body mesh must not contain the bumper that tears off.
GPU path
Section titled “GPU path”sparklight_3d::dynamic_mesh: Renderer3D::update_mesh_vertices(model, mesh, &[Vertex3D]) rewrites a mesh’s vertex buffer in place (write_buffer, nothing allocated; a thousand updates leave the resource counts unchanged) and
refreshes the bounds the culling and shadows use; InvalidMesh explains a refusal (wrong vertex count, skinned mesh, unknown model). DynamicMesh is the CPU side with a version. The scene’s ModelCache uses it: a deformable object is drawn from the key
fx:deform:<id>, uploaded once, and each frame its version changed the new vertices are queued and ModelCache::release_stale(renderer) (called by space3d.capture and spark run) writes them. The mesh is built when the cache hands over the source model, so
a headless run that never renders does no mesh work and has the same state hash as a rendered one (tested).
Impact damage on any body
Section titled “Impact damage on any body”VehicleDamage (despite the name) is the general impact-damage component: put it on any object with a dynamic Body and it records dents from contact forces, tracks a body health from 1 to 0, and holds DetachableParts; add DeformableMesh to show the dents on the
object’s mesh. Nothing in it knows about cars except the engine, wheel and suspension health, which only matter when the object also has a Vehicle.
{"command": "space3d.spawn", "params": {"name": "Crate", "components": { "Transform3D": {"position": [0, 0.6, 0]}, "MeshRenderer": {"model": "crates/crate.glb"}, // any glTF whose origin is the body's origin "Body": {"kind": "Dynamic", "shape": {"Box": {"half_extents": [0.6, 0.6, 0.6]}}, "density": 250}, "VehicleDamage": {"overrides": {"speed_threshold": 3.0, "deform": {"max_deform": 0.25}}}, "DeformableMesh": {}}}}{"command": "space3d.spawn", "params": {"name": "Lid", "components": { // a part: an object standing where it sits on the crate "Transform3D": {"position": [0, 0.6, 0.62]}, "MeshRenderer": {"model": "crates/lid.glb"}, "DetachablePart": {"vehicle": "Crate", "half_extents": [0.5, 0.5, 0.05], "break_force": 40000, "break_impulse": 600}}}}{"command": "space3d.damage.hit", "params": {"entity": "Crate", "speed": 8, "point": [0, 0.6, 0.6], "direction": [0, 0, -1]}}{"command": "space3d.damage.repair", "params": {"entity": "Crate"}}The hit threshold (speed_threshold), the dent depth and radius, the body health lost per m/s, the crumple regions and the limits are all DamageDesc / DeformDesc data; the engine_*, wheel_*, suspension_* and steer_pull fields do nothing without a Vehicle. Events for a body are
body_impact and body_repaired (a vehicle reports vehicle_impact and vehicle_repaired); part_detached and part_expired are the same for both. In Rust: VehicleDamage::on_body(&mut world, body, desc), step_body, hit_body, repair_body.
Replacing the object’s Body component recreates the body and its damage model starts fresh (parts re-attach where they were). A fixed or kinematic body cannot carry a damage model (E_FX_DAMAGE_DESC). Measured in a_plain_dynamic_body_takes_dents_loses_a_part_and_is_repaired_without_a_vehicle:
a 300 kg crate hitting a wall at about 20 m/s takes one hit of 0.64 m of recorded depth, drops to 57 percent body health and loses its door; any_dynamic_body_dents_loses_a_part_and_reports_body_events does the same through a scene with a deformed mesh.
Components
Section titled “Components”| Component | Fields |
|---|---|
Vehicle |
preset (sedan, sport, rally), overrides (merge patch of VehicleDesc), visual (auto: box + wheel cylinders unless the object has a MeshRenderer; proxy; none), wheel_mesh and wheel_mesh_scale (a model drawn at every wheel, axle along X, left wheels mirrored), color |
VehicleDamage |
overrides (merge patch of DamageDesc, no parts). On an object with a Vehicle it also drives the engine, wheel and suspension effects; on an object with a dynamic Body it is plain impact damage (the object waits until it has one of the two) |
DeformableMesh |
source (model key, default the object’s MeshRenderer model), overrides (merge patch of DeformDesc). Needs VehicleDamage |
DetachablePart |
vehicle (object name or id), half_extents, mass, break_force, break_impulse, reach, detach_speed. The part’s current pose relative to the chassis is the attachment; no Parent, no Body |
A part’s transform follows the chassis, then its debris body (interpolated between steps like any body), and shrinks to nothing when the debris fades. Editing the Vehicle rebuilds it, its damage model, and
re-attaches the parts where they were attached. Removing a part object takes its collider off the chassis; removing VehicleDamage takes all part colliders off.
Events (space3d.events): vehicle_impact {speed, depth, force, point, body_health}, part_detached {part, vehicle, force} (entity = the part’s object), part_expired, vehicle_repaired.
Commands
Section titled “Commands”| Command | Does |
|---|---|
space3d.vehicle.input |
entity, throttle, brake, steer, handbrake, clutch, shift (one-shot), gear (one-shot), analog; held until changed |
space3d.vehicle.telemetry |
the telemetry above plus the damage state (read-only) |
space3d.vehicle.damage |
speed (m/s of speed change), point, direction; or detach_part (a part object’s name) |
space3d.vehicle.repair |
restore everything |
space3d.vehicle.tune |
patch: merge patch of the VehicleDesc; the patch is kept in the component |
space3d.vehicle.reset |
position, rotation or yaw, repair: back on the road, upright and still |
All of them are logged by space3d.log and replay to the same state hash (tested through a replay of the log). Assertions: {"kind": "feature", "entity": "Car", "field": "VehicleDamage.body", "max": 0.6}
(also Vehicle.speed, Vehicle.skid, VehicleDamage.parts_detached, VehicleDamage.deepest_dent, VehicleDamage.power) and {"kind": "event", "event": "part_detached", "entity": "BumperFront"}.
Determinism and tests
Section titled “Determinism and tests”- The driving state (
Vehicle::hash_into), the damage state (VehicleDamage::hash_into: systems, the impact record, parts) and the physics bodies are inspace3d.hash; snapshots copy them (restore and step on gives the same bits). crates/sparklight-physics-fx/tests/vehicle_driving.rs: tire curves, load sensitivity, static deflection, damping and bump stop, anti-roll, gearbox (hunting, downshifts, manual, neutral, reverse, clutch), rev limiter, launch, differentials, AWD, ABS, handbrake drift, steering feel, right-turn convention, downforce and drag, telemetry and surfaces, live tuning, and two child processes producing the same hash.vehicle_damage.rs: deformation bounded and reproducible (bit for bit, independent of how often it is updated), no cracks, unit normals, regions, a car into a wall (dent, bumper off, engine damaged), a bump that does nothing, damage growing with speed, parts at their thresholds and by fatigue, repair, debris lifetime, damage changing handling.crates/sparklight-scene3d/tests/vehicle.rs: the components in a Space3D, telemetry, one-shot shifts, tuning, reset, a crash end to end, parts following the chassis and their debris, the mesh as a picture of the record (late and early meshes are identical, drawing does not change the hash), snapshots, repair, rebuilds, scene files.crates/sparklight-control/tests/space3d_vehicle.rs: the commands, MCP tools, the log replay, the scenario with goldens, and a capture of a crumpled car that differs from a new one and equals it again after a repair.crates/sparklight-3d/tests/dynamic_mesh.rs: in-place vertex updates on a real GPU (moved vertices change the picture, restoring them restores it exactly, bounds follow, wrong updates are refused, no allocation).- Goldens
goldens/3d/vehicle_sedan_new.pngandvehicle_crashed_sedan.png(created by the scenario withupdate_goldens, inspected before committing).
Cost (this Mac, release build, one thread; cargo run --release -p sparklight-physics-fx --example vehicle_bench)
Section titled “Cost (this Mac, release build, one thread; cargo run --release -p sparklight-physics-fx --example vehicle_bench)”Per car per step: Vehicle::step 1.5 to 2 microseconds with one to ten cars (up to 8 with fifty cars steering into each other), the physics step 1.4 to 1.6 microseconds per car (chassis and contacts), the damage model 0.1 microseconds per car with parts
and no contact events. So a 60 Hz frame (16.67 ms) fits about 1,900 cars with damage at the 50-car cost measured (4,000 at the 10-car cost). A hit costs one apply_impact (37 microseconds on the 3.6 k vertex body).
Known limits
Section titled “Known limits”- One collider (a box) for the chassis and one box per part; the visual mesh does not collide. The dents do not change the physics shape.
- Deformation is plastic (no spring-back) and visual: a dent is a displacement of vertices, not a change of mass or aerodynamics (the drag and engine effects come from the damage state).
- Damage is driven by the speed change of a hit, not by an energy model; rolling over, sliding along a wall and being hit by a light object are handled by the same threshold and the contact points.
- The model of a deformable body must have the chassis origin as its origin and no scale on the object; skinned meshes do not deform.