Home / Modules / Actor Motion / CarModelController

CarModelController

three + Rapier

Applies car motion state to visual transforms, including vehicle body pose and wheel animation.

CategoryActor Motion
Dependency tierthree + Rapier
Internal depsnone
Related modulesnone
Demo scenerace · Featured in this demo scene (coming soon)
Take just this module
modules/actor-motion/ground-vehicle/CarModelController.js

Copy it together with its internal dependencies, preserving the relative directory structure.

Source

import { Matrix4, Vector3 } from 'three';

const EPS = 1e-6;

export class CarModelController {
  constructor({
    vehicleModel = null,
    wheels = [],
    wheelPivots = [],
    wheelRadius = 0.35,
    steerWheelIndices = [0, 1]
  }) {
    this.vehicleModel = vehicleModel;
    this.wheels = wheels;
    this.wheelPivots = wheelPivots;

    this.wheelRadius = wheelRadius;
    this.steerWheelIndices = new Set(steerWheelIndices);

    this.wheelSpin = 0;
    this.modelMatrix = new Matrix4();
    this.forwardVelocity = new Vector3();
    this.modelBack = new Vector3();
  }

  reset(position) {
    this.wheelSpin = 0;

    if (this.vehicleModel) {
      this.vehicleModel.position.copy(position);
      this.vehicleModel.quaternion.identity();
    }

    for (let i = 0; i < this.wheels.length; i += 1) {
      const wheel = this.wheels[i];
      const pivot = this.wheelPivots[i];

      if (wheel) {
        wheel.rotation.x = 0;
        wheel.rotation.y = 0;
      }
      if (pivot) pivot.rotation.y = 0;
    }

    return this.vehicleModel;
  }

  step({
    position,
    bodyFrame,
    velocity,
    steeringAngle, // Rapier3D local yaw angle around up (+Y)
    deltaSeconds = 1 / 60
  }) {
    this.updateChassis(position, bodyFrame);
    this.updateWheels(bodyFrame, velocity, steeringAngle, deltaSeconds);

    return this.vehicleModel;
  }

  updateChassis(position, bodyFrame) {
    if (!this.vehicleModel) return;

    this.vehicleModel.position.copy(position);

    if (bodyFrame?.right && bodyFrame?.up && bodyFrame?.forward) {
      // Matrix4.makeBasis asks where local +Z points; since vehicle meshes face local -Z, local +Z points to the backward direction.
      this.modelBack.copy(bodyFrame.forward).multiplyScalar(-1);
      this.modelMatrix.makeBasis(bodyFrame.right, bodyFrame.up, this.modelBack);
      this.vehicleModel.quaternion.setFromRotationMatrix(this.modelMatrix);
    }
  }

  updateWheels(
    bodyFrame,
    velocity,
    steeringAngle, // Rapier3D local yaw angle around up (+Y)
    deltaSeconds
  ) {
    const radius = Math.max(EPS, Math.abs(this.wheelRadius));
    this.wheelSpin += (this.getForwardSpeed(velocity, bodyFrame) * deltaSeconds) / radius;
    const localYaw = steeringAngle;

    for (let i = 0; i < this.wheels.length; i += 1) {
      const wheel = this.wheels[i];
      const pivot = this.wheelPivots[i];
      const wheelYaw = this.steerWheelIndices.has(i) ? localYaw : 0;

      wheel.rotation.x = this.wheelSpin;

      if (pivot) {
        pivot.rotation.y = wheelYaw;
        wheel.rotation.y = 0;
      } else {
        wheel.rotation.y = wheelYaw;
      }
    }
  }

  getForwardSpeed(velocity = null, bodyFrame = null) {
    if (!velocity || !bodyFrame?.forward) return 0;
    return this.forwardVelocity.copy(velocity).dot(bodyFrame.forward);
  }
}