Vehicle
Машина — это два аспекта, каждый на узле, который он описывает:
| где | что это | |
|---|---|---|
Vehicle |
узел шасси | машина целиком: масса, двигатель, руль, аэродинамика — и педали |
Wheel |
узел каждого колеса | это колесо: размер, подвеска, шина, его тормоз — и его показания |
SDK — это машина как железо: входы (throttle, steer, steerForce, ratio, clutch, brake на
каждое колесо) и датчики (rpm, speed, yawRate, driftAngle, по-колёсные slip, load,
силы…). Всё, что решает водитель, — дело игры: какая передача, когда схватывает сцепление, может ли
двигатель заглохнуть, как руль сужается со скоростью, ABS, ручник, позирование моделей колёс — каждое
по несколько строк над этими показаниями. Коробки передач в SDK нет:
коробка передач — дело игры.
Разделение повторяет движок: Jolt хранит настройки подвески и шины на колесо, SDK тоже. Узлу
шасси нужен Shape (его тело столкновений), и он не должен нести ещё и аспект
Physics — Vehicle владеет своим твёрдым телом (прикрепить его рядом бросает исключение), как
CharacterController владеет своим контроллером. Контакты по-прежнему работают:
car.addEventListener('enter', …) срабатывает как для любого тела.
Обзор
Physics.configure({ gravity: [0, -9.81, 0] })
const scene = new Scene()
const road = Material.lit({ color: '#2b3550' })
const paint = Material.lit({ color: '#c85a3c' })
scene.add(Mesh.box({ material: road, size: [40, 1, 400], position: [0, -0.5, -100] })
.aspect(Shape, {})
.aspect(Physics, { motion: 'static', friction: 1 })) // асфальт: шины читают трение ЗЕМЛИ
const R = 0.35 // радиус колеса, м
const hub = (x: number, z: number) => {
const n = new Node()
n.position = [x, R - 0.6, z] // где центр колеса покоится на ровной земле
return n.aspect(Wheel, { radius: R, width: 0.25 })
}
const chassis = new Node()
chassis.position = [0, 0.6, 0]
chassis.add(
Mesh.box({ material: paint, size: [1.8, 0.6, 4.2], position: [0, 0.05, 0] }),
hub(-0.8, -1.4), hub(0.8, -1.4), hub(-0.8, 1.4), hub(0.8, 1.4), // вперёд — это −Z: передняя ось
)
const car = chassis.aspect(Shape, { box: [0.9, 0.4, 2.1] }).aspect(Vehicle, { mass: 1200 })
scene.add(car)
class Driver extends System<'driver'> {
updateBefore() { // РАНО: шаг физики этого кадра едет на этом
const v = car.vehicle
const fwd = (Input.key('KeyW') ? 1 : 0) - (Input.key('KeyS') ? 1 : 0)
v.throttle = Math.abs(fwd) // 0..1, без знака
v.ratio = fwd > 0 ? 2.66 : fwd < 0 ? -2.9 : 0 // передача: её ЗНАК — направление, 0 = нейтраль
v.steer = (Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0)
for (const w of v.wheels) w.brake = Input.key('Space') && !w.steer ? 4500 : 0 // Н·м: ручник
}
}
scene.system(Driver)
scene.open()Это весь минимум. Vehicle находит каждое Wheel под шасси, группирует их в оси по Z (передняя
первой), и умолчания получаются сами: передняя ось рулит, задняя — ведущая. Любое из этого
переопределяется на колесо. У машины пока нет видимых колёс — SDK ничего не позирует; см.
позирование моделей колёс.
Пространство и ориентация
Пространство шасси — вперёд −Z, вверх +Y — собственная конвенция SDK, та, что сообщает
node.forward. Если модель смотрит в другую сторону, поверни модель (дочерний узел), никогда не
узел шасси.
Позиция узла колеса — где его центр покоится на ровной земле. Ставь её ниже начала координат шасси: колесо, заданное выше, висит в воздухе, и машина тогда крутит мотор впустую. SDK никогда не двигает сам узел колеса — где колесо находится сейчас, это показание.
Физика владеет трансформом шасси. Управляй им через входы, а не покадровыми записями
position. Запись position действительно ставит машину (она доходит до тела шасси, как у любого
физического узла), но сохраняет скорость машины и её крен — чтобы вернуть машину на дорогу, используй
reset(p).
Входы — педали
car.vehicle.throttle = 1 // 0..1 — педаль двигателя; без знака
car.vehicle.ratio = 2.66 // ЕДИНСТВЕННОЕ передаточное число, на котором работает двигатель (по умолчанию 1): отрицательное = задний ход, 0 = нейтраль
car.vehicle.clutch = 1 // 0..1 включение (по умолчанию 1); 0 = двигатель отсоединён от колёс
car.vehicle.steer = -0.5 // −1..1, вправо — положительное
car.vehicle.steerForce = 1 // 0..1 — как крепко руки держат руль с `feedback` (по умолчанию 1)
wheel.brake = 1500 // Н·м тормозного момента на ЭТОМ колесе (по умолчанию 0)Пиши их из updateBefore системы: машина передаёт их движку в том же раннем проходе (она тикает
после порядка по умолчанию), так что шаг физики этого кадра едет на них. Они липкие — значение
продолжает действовать, пока ты не запишешь его снова, — а ненулевые throttle, steer или brake держат
машину бодрствующей (простаивающая машина без входа засыпает). throttle, steer, steerForce и
clutch зажимаются в свои диапазоны; отрицательный brake считается 0.
ratio— выбор всей трансмиссии: обороты двигателя = обороты колеса ×ratio× 3.42 (главная передача). Его знак — направление, так что задний ход — просто отрицательная передача.steerбез руля с обратной связью — угол дорожных колёс как доляsteering.maxAngle, применяемый как есть — сужение со скоростью или ограничение скорости поворота (клавиатура просит полный выворот за один кадр) — дело игры. Сsteering.feedbackэто то, куда руки водителя направляют руль.- Тормоз — на колесо: ножной тормоз — момент на каждом колесе, ручник — задняя пара, ABS пульсирует ими. Тормозящее колесо зажимается на физическом пределе шины.
Коробка передач — дело игры
Выбор передачи — политика, а не физика: несколько строк над rpm и ratio. Пятиступенчатый автомат:
const RATIOS = [2.66, 1.78, 1.3, 1.0, 0.74]
const REDLINE = 6000 // = engine.maxRpm
class Automatic extends System<'automatic'> {
static after = [Driver] // переопределяет переднюю передачу Driver
gear = 0 // 0 нейтраль, 1… вперёд
private wait = 0
updateBefore(dt: number) {
const v = car.vehicle
this.wait = Math.max(0, this.wait - dt)
if (this.gear === 0 && v.throttle > 0) this.gear = 1
if (this.gear > 0 && this.wait === 0) {
if (v.rpm > REDLINE * 0.67 && this.gear < RATIOS.length) { this.gear++; this.wait = 0.5 }
else if (v.rpm < REDLINE * 0.33 && this.gear > 1) { this.gear--; this.wait = 0.5 }
}
if (v.ratio > 0) v.ratio = this.gear === 0 ? 0 : RATIOS[this.gear - 1]
}
}
scene.system(Automatic)Секвентальная механика — клавиша, переключающая передачу; огибающая сцепления во время
переключения — clutch, плавно проведённый от 0 обратно к 1. Ни того ни другого в SDK нет намеренно.
Vehicle — машина целиком
node.aspect(Vehicle, {
mass?: number // кг, по умолчанию 1200. Колёса в этой модели безмассовые
centerOfMass?: number // высота в пространстве шасси, м; по умолчанию: середина формы по высоте
driveAxle?: 'rear' | 'front' | 'all' | 'none' // по умолчанию 'rear'; 'none' = прицеп, толкаемая тележка
differential?: 'lsd' | 'open' | 'locked' | number // по умолчанию 'lsd'
antiRoll?: boolean | number // по умолчанию true (0.01 от пружины колеса)
maxTilt?: number // градусы, после которых ограничение выправляет машину; по умолчанию 60 (180 = выкл)
friction?: number // трение тела ШАССИ — не шин; по умолчанию 0.6
clutchStrength?: number // по умолчанию 10
clutchCapacity?: number // Н·м, после которых сцепление проскальзывает; по умолчанию 0 = без предела
engine?: { torque?, maxRpm?, idleRpm?, curve?, inertia?, braking? }
steering?: { maxAngle?, caster?, trail?, trailAtPeak?, feedback?, driver? }
aero?: { downforce?, drag? }
})engine — включая его характер
engine: { torque: 600, maxRpm: 7000, curve: [{ x: 0, y: 0.8 }, { x: 0.66, y: 1 }, { x: 1, y: 0.8 }] }torque (Н·м, по умолчанию 500) — самая главная ручка «насколько она быстрая», а maxRpm (по
умолчанию 6000) — отсечка, собственный зажим двигателя; заикание ограничителя оборотов — дело игры.
curve — то, что делает двигатели разными на ощупь: точки { x, y } в нормализованных единицах —
x — доля отсечки, y — доля torque, x по возрастанию. Пример выше — обычный бензиновый двигатель, с
пиком на двух третях и спадом к отсечке; [{ x: 0, y: 1 }, { x: 1, y: 1 }] — плоская, так ощущается
электромотор. Без неё берётся собственная широкая кривая Jolt.
inertia (кг·м², по умолчанию 0.5) — как охотно он набирает обороты с выжатым сцеплением, braking
(по умолчанию 0.2) — торможение двигателем, а idleRpm (по умолчанию 1000) — пол, ниже которого он
не опускается. idleRpm: 0 убирает пол: двигатель тогда можно затянуть до остановки — заглохнуть, — а
удержание холостых (регулятор над throttle) или запуск стартером — дело игры.
Сцепление — clutchStrength, clutchCapacity
clutchStrength (по умолчанию 10) — как жёстко полностью включённое сцепление связывает двигатель и
колёса; вход clutch масштабирует его 0..1. Малые значения проскальзывают, большие — сваривают
намертво. При clutchCapacity 0 у сцепления нет потолка момента. Задай его (у дорожной машины ~1.5×
пикового момента двигателя) — и сцепление проскальзывает на этих Н·м × clutch — сухое сцепление:
старт можно дозировать, а сцепление, брошенное на слишком высокой передаче, тянет двигатель вниз
вместо того, чтобы дёрнуть машину. При idleRpm: 0 это затягивание — глохнущий мотор.
differential — и кнопка дрифта
lsd — ограниченное проскальзывание (коэффициент Jolt 1.4 — разумное умолчание). open отдаёт
вывешенному или буксующему колесу весь момент. locked заставляет оба колеса оси крутиться с одной
скоростью, так что пара срывается вместе, а не по одному — это и есть дрифтовая настройка, и
переключиться на неё на ходу — одна строка (см. перенастройку). Число — сырой
коэффициент max/min скоростей колёс (> 1; меньше — сильнее блокировка).
aero — почему машина должна поворачивать ЛУЧШЕ, чем быстрее едет
Оба числа задаются одинаково — как доля собственного веса машины на 30 м/с (108 км/ч) — и оба
растут с квадратом скорости от этой точки, так что 0.5 на 30 м/с — это 2.0 на 60 м/с. Одна
мысленная модель для обоих и никаких коэффициентов сопротивления или лобовых площадей, которые надо
выдумывать.
Сцепление шин — константа, а сила, которую требует поворот, растёт с v². Без прижимной силы (по
умолчанию 0) любая машина поэтому на скорости вылетает с дороги — именно это и значит «она не
поворачивает на 100 км/ч». Прижимная сила тоже растёт с v², так что они идут вровень: 0.6 примерно
удваивает способность машины поворачивать на 150 км/ч. Дорожная машина ближе к 0.1, гоночная — далеко
за 1. Она давит вдоль собственной оси «вниз» шасси (это и держит машину прижатой на гребне) и
только пока колесо на земле — машину на крыше её собственное антикрыло никогда не запустит.
drag (по умолчанию 0.03) — то, что вообще даёт машине максимальную скорость.
antiRoll — насколько кренится кузов
true (по умолчанию) — стабилизатор жёсткостью 0.01 от собственной пружины колеса; число — этот
коэффициент напрямую, 0 / false убирает стабилизаторы. Коэффициент, а не Н/м, так что он значит то
же самое, когда ты меняешь mass или suspension.stiffness колеса.
Эти числа куда меньше, чем выглядят, и прирост крутой. Стабилизатор Jolt — не пружина параллельно
подвеске: он подаёт lengthDifference × stiffness × dt как смещение в собственное мягкое ограничение
подвески, так что коэффициент, читающийся как «1% пружины», уже значим. Измерено на машине 1250 кг,
тот же поворот:
| коэффициент | крен при 0.86 g | крен при ~1.5 g |
|---|---|---|
0 |
1.3° | 2.5° |
0.01 (true) |
0.6° | — |
0.04 |
— | 0.4° |
0.15 |
— | 0.2° — кузов остаётся плоским |
0.5 |
машина ПРЫГАЕТ: стабилизатор отключается при отрыве колеса и работает как реле |
Так что это ручка того, насколько машина заметно кренится. Это не ручка отрыва колеса:
стабилизаторы не меняют, сколько нагрузки переходит между сторонами, только насколько кузов
наклоняется, пока это происходит. Для отрыва понижай centerOfMass.
steering — выворот, тяга шин и руль с телом
steering: { maxAngle: 35 } // по умолчанию: steer = ±1 — это ±35°
steering: { maxAngle: 35, feedback: true } // руль, на который давят в ответ шины
steering: { feedback: { powerSteering: 0.3 }, driver: { maxTorque: 90 } }maxAngle (градусы, по умолчанию 35) — что значит steer = ±1. Геометрия передней оси — caster
(механическое плечо, м, по умолчанию 0.03), trail (пневматическое плечо шины, м, по умолчанию 0.025)
и trailAtPeak (что от него остаётся на пиковом угле увода шины, по умолчанию 0.5) — рычаг
стабилизирующего момента шин, который сообщает steerTorque (Н·м, положительный тянет руль
вправо): тяжёлый на скорости, лёгкий, когда перед срывается. Он есть с рулём обратной связи и без —
сигнал, которым управляется руль с force feedback.
feedback делает руль телом, которое движок интегрирует каждый подшаг. Тогда steer — куда его
направляют руки водителя, а steerForce — как крепко они его держат; где окажутся дорожные колёса —
физика: на скорости зажатая клавиша оседает, не доходя до упора, — там, где момент рук уравновешивает
тягу шин; отпусти (steerForce = 0) — и руль сам центрируется в повороте на сцеплении или уходит в
контрруление в заносе. feedback: true берёт умолчания; объект настраивает колонку:
feedback |
по умолчанию | |
|---|---|---|
inertia |
1 | кг·м² — руль + колонка + рейка |
damping |
25 | Н·м·с/рад — не даёт отпущенному рулю звенеть вокруг центра |
friction |
8 | Н·м — собственное сухое трение колонки |
parkingFriction |
40 | Н·м на (почти) стоящей машине, сходит на нет к 1.5 м/с — почему стоящую машину тяжело рулить |
powerSteering |
0.5 | 0..1 — доля стабилизирующего момента, которую руль никогда не видит (1 = мёртвый обод) |
endStop |
{ band: 16, torque: 120, damping: 150 } |
резина перед упором: градусы, Н·м на упоре, Н·м·с/рад × глубина |
driver — руки на руле с обратной связью: maxTorque (Н·м, по умолчанию 130 — потолок того, сколько
выворота зажатая клавиша удерживает против шин), stiffness (Н·м/рад, по умолчанию 500), maxRate
(град/с, с которой руки могут поворачивать дорожные колёса, по умолчанию 120) и rampTime (с, за
которые нарастает момент рук, по умолчанию 0.1). Что руки делают — направляют, держат, отпускают —
дело игры, покадрово, через steer и steerForce.
Wheel — одно колесо
Аспект на собственном узле колеса. Его позиция — положение колеса в покое; всё остальное об этом колесе живёт здесь, где его хранит и Jolt.
hub.aspect(Wheel, {
radius?: number // м; по умолчанию: измеряется по отрисованным границам узла, иначе 0.35
width?: number // м; по умолчанию: измеряется, иначе 0.25
steer?: boolean // по умолчанию: передняя ось
driven?: number // ДОЛЯ момента двигателя на этом колесе; по умолчанию 1 на driveAxle, иначе 0
axle?: number // группа оси, спереди назад; по умолчанию: группируется по Z
suspension?: { travel?, stiffness?, damping? } // м, Гц, коэффициент; по умолчанию 0.4, 1.4, 0.5
tire?: Tire // по умолчанию Tire.road()
})
hub.wheel.brake = 4500 // вход: Н·м в этом кадре (см. Входы)driven — доля, а не флаг: доля оси — сумма её двух колёс, деление лево/право — их отношение — 0.6 /
0.4 перед-зад — это driven: 0.3 на каждом переднем колесе и 0.2 на каждом заднем. axle
объединяет колёса в пары для дифференциала и стабилизатора; задавай его, когда колёса не стоят
аккуратными парами лево/право (колёса в пределах 0.35 м друг от друга по Z по умолчанию делят ось).
tire — кривые как точки
new Node().aspect(Wheel, { tire: Tire.drift() }) // шина, которую можно держать боком
for (const w of car.vehicle.wheels) w.node.aspect(Wheel, { tire: Tire.scale(Tire.road(), 0.7) }) // мокрая дорога, на ходуменяй шину живой машины, переконфигурируя аспект колеса, как выше — машина перенастраивается
перед следующим шагом. Прямое присваивание wheel.tire не доходит до движка, пока что-то другое не
перенастроит машину.
Tire — числа до самого низа, как и кривая момента двигателя:
interface Tire {
side: Point[] // боковое трение по УГЛУ увода: { x: градусы, y: трение }, x по возрастанию
forward: Point[] // продольное трение по КОЭФФИЦИЕНТУ проскальзывания: { x: коэффициент, y: трение }
traction?: number // зажим ПРИВОДНОГО импульса как кратное трение × нагрузка (1 = физическая шина)
circle?: number // 0..1 круг трения: сколько бокового сцепления теряет буксующее / заблокированное колесо
}
Tire.road() // штатная шина Jolt: боковой пик 1.2 на 3°, 1.0 от 20°; traction 10, circle 0 — приклеена
Tire.arcade() // пик держится до конца по обеим осям: машина не срывается
Tire.drift() // боковое сцепление падает до 0.7 после 25°, traction 2, circle 0.85 — избыточная поворачиваемость под газом
Tire.scale(t, k) // обе кривые × k — ползунок сцепления, холодная шина; новая Tire, t не тронута
resolveTire(t) // что получает движок: пустые кривые и отсутствующий traction берут значения road, circle зажат в 0..1Пресеты — свежие списки при каждом вызове — скопируй один и правь. Значение traction у road, 10
(собственное значение из примера Jolt), означает, что ведущее колесо практически никогда не срывается в
пробуксовку; всему, во что должен вцепляться круг трения, нужен traction около 1–2. То, что колесо
получает на деле, объединяется с землёй, на которой оно стоит, как sqrt(tire × groundFriction), а
трение тела по умолчанию 0.6 — так что дороге, по которой машина должна по-настоящему поворачивать,
нужен Physics { friction: 1 }.
suspension — один travel не даёт колесу больше хода
Положение колеса в покое — там, где пружина несёт вес машины, и этот прогиб равен g / (2πf)² — он
зависит только от stiffness, не от travel. Прогиб — это ровно и то, насколько колесо ещё может
отвиснуть. Так что повышение одного travel покупает только запас на сжатие; чтобы колёса
доставали дальше вниз — в ямы, с гребней, при приземлении, — смягчи и пружину. При умолчаниях
(travel: 0.4, stiffness: 1.4, damping: 0.5) это 12.7 см отвисания и 11.3 см сжатия.
Позирование моделей колёс
SDK ничего не позирует: position колеса (пространство шасси, вдоль хода подвески), steerAngle и
spin — то, из чего строится модель колеса — в поздней фазе, после шага. Клади каждую модель на
узел, который — сосед своей ступицы под шасси:
const DEG2RAD = Math.PI / 180
const tire = Material.lit({ color: '#20242e' })
const art = new Map<Wheel, Node>()
for (const w of car.vehicle.wheels) {
const disc = Mesh.cylinder({ material: tire, radius: R })
disc.scale = [1, 0.25, 1]
disc.eulerAngles = [0, 0, 90] // ось цилиндра — +Y; ось колеса — +X
const holder = new Node()
holder.add(disc)
chassis.add(holder)
art.set(w, holder)
}
class WheelArt extends System<'wheelArt'> {
private angle = new Map<Wheel, number>()
update(dt: number) { // ПОЗДНО: устоявшиеся показания
for (const w of car.vehicle.wheels) {
const a = ((this.angle.get(w) ?? 0) + w.spin * dt) % (2 * Math.PI)
this.angle.set(w, a)
const n = art.get(w)!
n.position = w.position
n.quaternion = Quat.fromAxisAngle([0, 1, 0], -w.steerAngle * DEG2RAD).mul(Quat.fromAxisAngle([1, 0, 0], -a))
}
}
}
scene.system(WheelArt)Оба минуса — это движок, сообщающий в терминах водителя — руль положителен вправо, вращение положительно вперёд — в правосторонней сцене, где вперёд −Z. Тормозной суппорт — то же самое без вращения.
reset и импульсы
car.vehicle.reset(position?, rotation?): this // телепорт в вертикальное положение, сброс всего движения
car.vehicle.applyImpulse(v): this // кг·м/с на шасси — площадка ускорения, взрывreset() без аргументов переворачивает машину обратно там, где она лежит (сохраняет курс, сбрасывает
крен и тангаж). Это единственное, что обнуляет скорости, обороты двигателя и вращение колёс — простая
запись position этого не делает. Входы сохраняются и отправляются снова в следующем раннем проходе.
Перенастройка живой машины
Переконфигурирование прикреплённого аспекта перенастраивает его на месте — так работает кнопка дрифта:
car.aspect(Vehicle, { differential: drift ? 'locked' : 'lsd' })Живое: differential, engine, steering, aero, maxTilt, friction, clutchStrength,
clutchCapacity, antiRoll (его жёсткость, на осях, у которых есть стабилизатор) и tire каждого
колеса (переконфигурируй это колесо: w.node.aspect(Wheel, { tire })).
Структурное — запечено в ограничение при прикреплении: mass, centerOfMass, какие колёса
существуют и где, их suspension, доли driven. Изменение одного из них означает повторное
прикрепление аспекта; новая mass на живой машине предупреждает и оставляет построенную.
Чтение машины
car.vehicle.speed // м/с вдоль передней оси машины, отрицательная на заднем ходу (× 3.6 для км/ч)
car.vehicle.rpm
car.vehicle.wheelsOnGround / .grounded
car.vehicle.velocity // Vec3, мир, м/с
car.vehicle.angularVelocity // Vec3, мир, РАДИАНЫ/с
car.vehicle.yawRate // град/с вокруг вертикали, положительная при повороте ВПРАВО (тот же знак, что у steer)
car.vehicle.slipAngle // град между носом и движением кузова, + при движении правее него
car.vehicle.driftAngle // град ЗАНОСА: движение задней оси против оси трансмиссии
car.vehicle.steerAngle // град, где на самом деле стоят рулевые колёса, вправо — положительное
car.vehicle.steerTorque // Н·м, стабилизирующий момент шин, + тянет вправо
car.vehicle.wheels // аспекты Wheel, передняя ось первой
car.vehicle.id / .bodyId // нативные id машины / тела шасси (0 = не прикреплена)Входы тоже читаются обратно (throttle, steer, ratio, …). driftAngle равен 0 в чистом повороте
на любой скорости — в отличие от slipAngle, у которого кузов на шаге пешехода и полном вывороте едет
на ~20° в сторону от носа, — читается за 90° у машины, развёрнутой на передней передаче, и нарастает
на первых 1–2 м/с: это число, которое нужно гиро-ассисту или дрифт-метру.
На колесо, на самом колесе:
contact |
касается земли в этом шаге |
slip |
продольный коэффициент проскальзывания, со знаком: 0 катится синхронно, к +1 буксует, к −1 блокируется |
slipAngle |
градусы, со знаком: пятно, скользящее вправо от шины, — положительное. После ~3° она срывается |
steerAngle |
градусы, положительные вправо |
spin |
рад/с, положительное вперёд (× radius ≈ скорость, с которой шина прокладывает след) |
load |
Н — сила подвески на колесо (0 в воздухе) |
sideForce, forwardForce |
Н — силы шины в этом шаге, + вправо от неё / + толкая машину вперёд |
sideSlipSpeed, slipSpeed |
м/с — как быстро пятно скользит вбок / протектор бежит по дороге |
compression |
0 полностью разжата … 1 полностью сжата |
position |
Vec3, пространство шасси — где центр колеса сейчас |
// куда идут дым дрифта или следы шин: центры скользящих колёс, в мировом пространстве
const marks: Vec3[] = []
for (const w of car.vehicle.wheels) {
if (w.contact && Math.abs(w.slipAngle) > 8) marks.push(car.worldMatrix.transformPoint(w.position))
}Гейтинг хоста
Всё здесь требует 3D-физики — защищай через Physics.supported. Без неё аспект прикрепляется, id
остаётся 0 и каждый вызов — no-op. Headless-рендерер отказывается от машины (она никогда не движется):
тестируй машину через lecodes render --sim или --desktop
(physics.md).
Не в v1
Мотоциклы и гусеничная техника (у Jolt есть оба контроллера), а также трансмиссия вообще без двигателя (аркадная модель «газ → сила»).