首页 / 模块目录 / 角色 / 载具运动 / CarModelController

CarModelController

three + Rapier

把车运动状态应用到视觉变换(车身姿态 + 车轮动画)。

类别角色 / 载具运动
依赖档位three + Rapier
内部依赖
相关模块
演示场race · 将随该演示场展示(即将上线)
只取这个模块
modules/actor-motion/ground-vehicle/CarModelController.js

连同其内部依赖一并复制,保留相对目录结构。

源码

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);
  }
}