@needle-tools/car-physics
Version:
Car physics for Needle Engine: Create physical cars with ease
306 lines • 14.1 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Behaviour, getBoundingBox, getParam, getTempQuaternion, getTempVector, Gizmos, Mathf, ParticleSystem, ParticleSystemBaseBehaviour, serializable } from "@needle-tools/engine";
import { Object3D, Vector2, Vector3, Quaternion, Euler } from "three";
import { CarAxle, CarDrive } from "./constants.js";
const debugWheel = getParam("debugwheels");
export class CarWheel extends Behaviour {
/** The wheel index in the car */
get index() { return this._wheelIndex; }
wheelModel;
axle = CarAxle.front;
/**
* The radius of the wheel.
* @default -1 = will be calculated based on the model
*/
radius = -1;
/**
* The rest length of the suspension.
* @default -1 = will be calculated based on the radius
*/
suspensionRestLength = -1;
/**
* The maximum travel distance of the suspension.
* @default -1 = will be calculated based on the radius
*/
maxSuspensionTravel = -1;
/**
* The suspension’s damping when the wheel is being compressed.
* Lower values make the suspension more bouncy while higher values make it more stiff.
* @default 3
*/
suspensionCompression = 3;
/**
* The relaxation of the suspension. Increase this value if the suspension appears to overshoot.
* @default 5
*/
suspensionRelax = 5;
/**
* The stiffness of the suspension. Increase this value if the suspension appears to not push the vehicle strong enough.
* @default -1
*/
suspensionStiff = -1;
/**
* The maximum force the suspension can exert.
* @default -1
*/
maxSuspensionForce = -1;
/**
* The multiplier of friction between a tire and the collider it’s on top of.
The larger the value, the stronger side friction will be.
*/
sideFrictionStiffness = 0.7;
/**
* The friction of the wheel based on the grip amount.
* Value Range: Lower values generally make the car more slippery while higher values make it more grippy. This is particular noticeable when steering.
* X: Friction used when the calculated wheel grip is low.
* Y: Friction used when the calculated wheel grip is high.
* @default { x: 1, y: 20 }
*/
frictionSlip = new Vector2(1, 20);
// --- Visuals ---
skidParticle;
skidVisualSideThreshold = 5;
skidVisualBreakThreshold = 0.1;
skidParticleBehaviour;
wheelModelRight;
wheelModelUp;
car;
vehicle;
_wheelIndex = -1;
_activeRadius = -1;
_initialQuaternion;
async initialize(car, vehicle, i) {
this.car = car;
this.vehicle = vehicle;
this._wheelIndex = i;
const target = this.wheelModel || this.gameObject;
// Automatically calculate the radius if it's not set
let radius = this.radius;
if (radius <= 0) {
const bounds = getBoundingBox(target);
radius = bounds.getSize(getTempVector()).y * .5;
}
if (radius < 0) {
console.error("CarWheel: Radius is invalid, please set it manually or make sure the wheel is attached to a model");
return;
}
this._activeRadius = Math.max(.01, radius);
// TODO: snap the up rotation (around car-relative Y) to the car's up rotation
this._initialQuaternion = target.quaternion.clone();
// Assuming that all car wheels have the same car-relative axis.
// Meaning car-relative X is to the side of the car and Z is forward
// If we don't reset the rotations here the wheel may be rotated in a weird way
// This should normally only an issue for vehicles with non-standard wheel orientations
this.wheelModel?.quaternion.identity();
this.gameObject?.quaternion.identity();
// Figure out which axis the wheel should rotate around
// Get the rotation in car space
// TODO: This is a bit hacky, but it works for now
const quat = car.gameObject.worldQuaternion.clone();
car.gameObject.worldQuaternion = new Quaternion();
const axesDiff = new Quaternion();
axesDiff.copy(car.gameObject.worldQuaternion)
.multiply(target.worldQuaternion.clone().invert());
car.gameObject.worldQuaternion = quat;
// Create rotation axis vectors
this.wheelModelUp = new Vector3(0, 1, 0)
.clone()
.applyQuaternion(axesDiff);
this.wheelModelRight = new Vector3(1, 0, 0)
.clone()
.applyQuaternion(axesDiff);
const wPos = target.worldPosition;
const lPos = this.car.gameObject.worldToLocal(wPos);
lPos.multiply(this.car.gameObject.worldScale);
// Move the wheel up by half radius (assuming our component is centered to the wheel model)
lPos.y += this._activeRadius * .5;
const suspensionDirection = getTempVector(0, -1, 0); // Y axis
const axleDirection = getTempVector(-1, 0, 0); // X axis
let restLength = this.suspensionRestLength;
if (!restLength || restLength <= 0) {
restLength = this._activeRadius * .5;
}
let maxSuspensionTravel = this.maxSuspensionTravel;
if (!maxSuspensionTravel || maxSuspensionTravel <= 0) {
maxSuspensionTravel = this._activeRadius * .5;
}
let suspensionStiff = this.suspensionStiff;
if (!suspensionStiff || suspensionStiff <= 0) {
suspensionStiff = 50;
}
let maxSupsensionForce = this.maxSuspensionForce;
if (!maxSupsensionForce || maxSupsensionForce <= 0) {
maxSupsensionForce = 100_000_000;
}
if (debugWheel)
console.debug(this.name, {
restLength,
suspensionTravel: maxSuspensionTravel,
suspensionStiff,
maxSupsensionForce,
radius: this._activeRadius
}, this);
this.vehicle.addWheel(lPos, suspensionDirection, axleDirection, restLength, this._activeRadius);
this.vehicle.setWheelMaxSuspensionTravel(i, maxSuspensionTravel);
this.vehicle.setWheelMaxSuspensionForce(i, maxSupsensionForce);
this.vehicle.setWheelSuspensionStiffness(i, suspensionStiff);
this.vehicle.setWheelSuspensionCompression(i, this.suspensionCompression);
this.vehicle.setWheelSuspensionRelaxation(i, this.suspensionRelax);
this.vehicle.setWheelSideFrictionStiffness(i, this.sideFrictionStiffness);
this.vehicle.setWheelFrictionSlip(i, this.frictionSlip.y);
if (this.skidParticle) {
this.skidParticleBehaviour = new SkidTrailBehaviour();
this.skidParticle.addBehaviour(this.skidParticleBehaviour);
}
}
applyPhysics(acceleration, breaking, steeringRad) {
const isOnDrivingAxle = (this.car.carDrive == CarDrive.front && this.axle == CarAxle.front)
|| (this.car.carDrive == CarDrive.rear && this.axle == CarAxle.rear)
|| (this.car.carDrive == CarDrive.all);
if (!isOnDrivingAxle)
acceleration = 0;
// accel & break
this.vehicle.setWheelEngineForce(this._wheelIndex, acceleration);
this.vehicle.setWheelBrake(this._wheelIndex, breaking);
// steer
if (this.axle == CarAxle.front) {
this.vehicle.setWheelSteering(this._wheelIndex, -steeringRad); // inverted X
}
// slip
const velocity = getTempVector(this.car.velocity).clampLength(0, 1);
let gripAmount = velocity.dot(this.car.gameObject.worldRight);
gripAmount = 1 - Math.abs(gripAmount);
const friction = Mathf.lerp(this.frictionSlip.x, this.frictionSlip.y, gripAmount);
this.vehicle.setWheelFrictionSlip(this._wheelIndex, friction);
}
updateVisuals() {
const target = this.wheelModel || this.gameObject;
// rotation
const wheelRot = this.vehicle.wheelRotation(this._wheelIndex);
const wheelTurn = this.vehicle.wheelSteering(this._wheelIndex);
const yRot = getTempQuaternion().setFromAxisAngle(this.wheelModelUp, wheelTurn);
const xRot = getTempQuaternion().setFromAxisAngle(this.wheelModelRight, wheelRot);
const wheelRotation = yRot.multiply(xRot);
target.quaternion.copy(wheelRotation);
target.quaternion.multiply(this._initialQuaternion);
// position
const contact = this.vehicle.wheelContactPoint(this._wheelIndex);
const isInContact = this.vehicle.wheelIsInContact(this._wheelIndex);
const wheelPosition = getTempVector();
if (contact) {
if (debugWheel)
Gizmos.DrawWireSphere(contact, .02, 0xffff55, 0, false);
wheelPosition.copy(this.car.gameObject.worldUp).multiplyScalar(this._activeRadius);
wheelPosition.add(contact);
target.worldPosition = wheelPosition;
// const wp = target.worldPosition;
// target.worldPosition = wp.lerp(wheelPosition, this.context.time.deltaTime / .05);
}
// skid
if (this.skidParticleBehaviour) {
const sideAmount = Math.abs(this.vehicle.wheelSideImpulse(this._wheelIndex) ?? 0);
const breakAmount = Math.abs(this.vehicle.wheelBrake(this._wheelIndex) ?? 0);
const isSkidding = sideAmount > this.skidVisualSideThreshold || breakAmount > this.skidVisualBreakThreshold;
const showSkid = isInContact && contact != undefined && isSkidding;
if (this.skidParticle && contact) {
const wPos = getTempVector(contact);
wPos.y += this.skidParticle.main.startSize.constant / 4; // offset the effect
this.skidParticle.worldPosition = wPos;
}
this.skidParticleBehaviour.isSkidding = showSkid;
}
// debug
if (debugWheel) {
const sphereSize = this._activeRadius * .1;
// draw wheel
const normal = getTempVector(this.car.gameObject.worldRight).multiplyScalar(-1);
normal.applyEuler(new Euler(0, wheelTurn, 0));
Gizmos.DrawCircle(wheelPosition, normal, this._activeRadius, 0x0000ff, 0, false);
const inner = getTempVector(wheelPosition);
const out = getTempVector(wheelPosition).add(getTempVector(normal).multiplyScalar(this._activeRadius));
Gizmos.DrawLine(inner, out, 0xff0000, 0, false);
Gizmos.DrawSphere(out, sphereSize, 0xff0000, 0, false);
const forward = getTempVector(this.car.gameObject.worldForward)
.multiplyScalar(this._activeRadius * 1);
forward.applyEuler(new Euler(0, wheelTurn, 0));
const end = getTempVector(wheelPosition).add(forward);
Gizmos.DrawLine(wheelPosition, end, 0x0000ff, 0, false);
Gizmos.DrawSphere(end, sphereSize, 0x0000ff, 0, false);
// draw susnpension line
// Gizmos.DrawLine(getTempVector(suspensionRest).add(getTempVector(right).multiplyScalar(-0.1)), getTempVector(suspensionRest).add(getTempVector(right).multiplyScalar(0.1)), 0xff0000, 0, false);
// Gizmos.DrawLine(getTempVector(suspensionRest).add(this.up.multiplyScalar(this.suspensionTravel)), getTempVector(suspensionRest).add(this.up.multiplyScalar(-this.suspensionTravel)), 0xebd834, 0, false);
}
}
}
__decorate([
serializable(Object3D)
], CarWheel.prototype, "wheelModel", void 0);
__decorate([
serializable()
], CarWheel.prototype, "axle", void 0);
__decorate([
serializable()
], CarWheel.prototype, "radius", void 0);
__decorate([
serializable()
], CarWheel.prototype, "suspensionRestLength", void 0);
__decorate([
serializable()
], CarWheel.prototype, "maxSuspensionTravel", void 0);
__decorate([
serializable()
], CarWheel.prototype, "suspensionCompression", void 0);
__decorate([
serializable()
], CarWheel.prototype, "suspensionRelax", void 0);
__decorate([
serializable()
], CarWheel.prototype, "suspensionStiff", void 0);
__decorate([
serializable()
], CarWheel.prototype, "maxSuspensionForce", void 0);
__decorate([
serializable()
], CarWheel.prototype, "sideFrictionStiffness", void 0);
__decorate([
serializable(Vector2)
], CarWheel.prototype, "frictionSlip", void 0);
__decorate([
serializable(ParticleSystem)
], CarWheel.prototype, "skidParticle", void 0);
__decorate([
serializable()
], CarWheel.prototype, "skidVisualSideThreshold", void 0);
__decorate([
serializable()
], CarWheel.prototype, "skidVisualBreakThreshold", void 0);
export class SkidTrailBehaviour extends ParticleSystemBaseBehaviour {
isSkidding = false;
update(particle, _delta) {
const trail = particle;
if (this.system.trails?.enabled && trail) {
// the most new particle wouldn't get affected
if (!this.isSkidding) {
particle.color.setW(0);
}
let tail = trail.previous?.tail;
while (tail && tail.hasPrev()) {
const myTail = tail;
myTail.data ??= {};
if (myTail.data["isSkidding"] === undefined) {
myTail.data["isSkidding"] = this.isSkidding;
}
if (myTail.data["isSkidding"] === false) {
tail.data.color?.setW(0);
}
tail = tail.prev;
}
}
}
}
//# sourceMappingURL=CarWheel.js.map