Files
gta6/three-player-controller/src/utils/vehiclePhysics/BVHVehicleController.ts
T
knsoandClaude Opus 5 fb38652e7f chore(controller): three-player-controller als Ordner statt Submodul
Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur
lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json.
Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser
Stand als normaler Ordner im Repo, ohne Verweis auf GitHub.

dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher
lokal gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:27:03 +02:00

486 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as THREE from "three";
import { VehicleCollision } from "./VehicleCollision";
import { ContactCache } from "../../collision/contacts/ContactCache";
import { ContactImpulseSolver } from "../../collision/solver/ContactImpulseSolver";
import { effectiveMass } from "./vehicleMath";
import type { VehicleRigidBody } from "./VehicleRigidBody";
import { VehicleWheel } from "./VehicleWheel";
const SIDE_DAMPING_RATE = 13; // 侧向速度阻尼率(1/s)
const FWD_FACTOR = 0.5; // 摩擦椭圆纵向权重
const SIDE_FACTOR = 1.0; // 摩擦椭圆侧向权重
/** 基于 three-mesh-bvh 射线与轻量刚体的 Raycast Vehicle 求解器。 */
export class BVHVehicleController {
readonly chassis: VehicleRigidBody; // 车身刚体
readonly wheels: VehicleWheel[] = []; // 车轮列表
private collider: THREE.Mesh[] = []; // 场景碰撞网格
private collision = new VehicleCollision();
private contactSolver = new ContactImpulseSolver();
private contactCache = new ContactCache();
private raycaster = new THREE.Raycaster();
private prevPosition = new THREE.Vector3();
private prevQuaternion = new THREE.Quaternion();
private normalMat = new THREE.Matrix3();
private steerAxis = new THREE.Vector3();
private steerQuat = new THREE.Quaternion();
private vel = new THREE.Vector3();
private impulse = new THREE.Vector3();
private r = new THREE.Vector3();
private gcross = new THREE.Vector3();
private local = new THREE.Vector3();
private world = new THREE.Vector3();
private invQuat = new THREE.Quaternion();
private upWS = new THREE.Vector3();
private sidePoint = new THREE.Vector3();
/** 绑定车身刚体,可选传入场景碰撞网格。 */
constructor(chassis: VehicleRigidBody, collider?: THREE.Mesh | THREE.Mesh[] | null) {
this.chassis = chassis;
this.collider = this.toColliderList(collider);
(this.raycaster as any).firstHitOnly = true;
}
/** 设置场景碰撞网格。 */
setCollider(collider: THREE.Mesh | THREE.Mesh[] | null | undefined): void {
this.collider = this.toColliderList(collider);
}
/** 返回车轮数量。 */
numWheels(): number {
return this.wheels.length;
}
// ==================== 轮子参数 ====================
/** 添加车轮并写入悬挂几何。 */
addWheel(
chassisConnectionCs: THREE.Vector3,
directionCs: THREE.Vector3,
axleCs: THREE.Vector3,
suspensionRestLength: number,
radius: number,
): VehicleWheel {
const wheel = new VehicleWheel();
wheel.connectionPoint.copy(chassisConnectionCs);
wheel.direction.copy(directionCs);
wheel.axle.copy(axleCs);
wheel.restLength = suspensionRestLength;
wheel.suspensionLength = suspensionRestLength;
wheel.visualLength = suspensionRestLength;
wheel.maxSuspensionTravel = suspensionRestLength;
wheel.radius = radius;
this.wheels.push(wheel);
return wheel;
}
/** 设置悬挂与底盘的局部连接点。 */
setWheelChassisConnectionPointCs(i: number, value: THREE.Vector3): void {
this.wheelAt(i)?.connectionPoint.copy(value);
}
/** 设置悬挂方向(底盘局部)。 */
setWheelDirectionCs(i: number, value: THREE.Vector3): void {
this.wheelAt(i)?.direction.copy(value);
}
/** 设置轮轴方向(底盘局部)。 */
setWheelAxleCs(i: number, value: THREE.Vector3): void {
this.wheelAt(i)?.axle.copy(value);
}
/** 设置悬挂静止长度。 */
setWheelSuspensionRestLength(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.restLength = value;
}
/** 设置轮胎半径。 */
setWheelRadius(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.radius = value;
}
/** 设置悬挂最大行程。 */
setWheelMaxSuspensionTravel(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.maxSuspensionTravel = value;
}
/** 设置悬挂刚度。 */
setWheelSuspensionStiffness(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.stiffness = value;
}
/** 设置压缩阻尼。 */
setWheelSuspensionCompression(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.dampingCompression = value;
}
/** 设置回弹阻尼。 */
setWheelSuspensionRelaxation(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.dampingRelaxation = value;
}
/** 设置最大悬挂力。 */
setWheelMaxSuspensionForce(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.maxSuspensionForce = value;
}
/** 设置制动力。 */
setWheelBrake(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.brake = value;
}
/** 设置转向角。 */
setWheelSteering(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.steering = value;
}
/** 设置驱动力。 */
setWheelEngineForce(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.engineForce = value;
}
/** 设置纵向抓地。 */
setWheelFrictionSlip(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.frictionSlip = value;
}
/** 设置侧向摩擦。 */
setWheelSideFrictionStiffness(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.sideFrictionStiffness = value;
}
/** 设置侧向力对侧倾的影响,0 为几乎不侧倾,1 为按真实触地点侧倾。 */
setWheelRollInfluence(i: number, value: number): void {
const wheel = this.wheelAt(i);
if (wheel) wheel.rollInfluence = value;
}
/** 读取转向角。 */
wheelSteering(i: number): number | null {
return this.wheelAt(i)?.steering ?? null;
}
/** 读取是否接地。 */
wheelIsInContact(i: number): boolean {
return this.wheelAt(i)?.isInContact ?? false;
}
/** 读取轮轴方向(底盘局部)。 */
wheelAxleCs(i: number): THREE.Vector3 | null {
return this.wheelAt(i)?.axle ?? null;
}
/** 读取悬挂与底盘的局部连接点。 */
wheelChassisConnectionPointCs(i: number): THREE.Vector3 | null {
return this.wheelAt(i)?.connectionPoint ?? null;
}
/** 读取当前悬挂长度。 */
wheelSuspensionLength(i: number): number | null {
return this.wheelAt(i)?.suspensionLength ?? null;
}
/** 读取视觉悬挂长度。 */
wheelVisualLength(i: number): number | null {
return this.wheelAt(i)?.visualLength ?? null;
}
/** 读取车轮自转角。 */
wheelRotation(i: number): number | null {
return this.wheelAt(i)?.rotation ?? null;
}
/** 读取接地网格。 */
wheelContactMesh(i: number): THREE.Mesh | null {
return this.wheelAt(i)?.contactMesh ?? null;
}
// ==================== 仿真步进 ====================
/** 推进一帧:悬挂与轮胎力 → 车身接触 → 积分 → CCD。 */
updateVehicle(dt: number, collider?: THREE.Mesh | THREE.Mesh[] | null): void {
if (collider !== undefined) this.collider = this.toColliderList(collider);
const clampedDt = Math.max(0, dt);
if (clampedDt === 0) return;
this.prevPosition.copy(this.chassis.position);
this.prevQuaternion.copy(this.chassis.quaternion);
this.updateWheelTransforms();
this.chassis.applyGravity(clampedDt); // 重力
this.raycastWheels(); // 轮子向下射线
this.solveSuspension(); // 悬挂弹簧阻尼
this.applySuspensionImpulses(clampedDt);
this.solveFriction(clampedDt); // 纵向驱动 / 侧向抓地
// 积分前解一次车身接触速度
const manifolds = this.collision.detect(this.chassis, this.collider);
this.contactCache.match(manifolds);
this.contactSolver.solveVelocity(this.chassis, manifolds, clampedDt);
this.contactCache.save(manifolds);
this.chassis.applyDamping(clampedDt);
this.chassis.integrate(clampedDt); // 积分位置和朝向
// 积分后再解一次,消化穿透
const positionManifolds = this.collision.detect(this.chassis, this.collider);
this.contactCache.match(positionManifolds);
this.contactSolver.solveVelocity(this.chassis, positionManifolds, clampedDt);
this.contactCache.save(positionManifolds);
// 高速时扫掠回退,避免穿模
if (this.collision.needsSwept(this.chassis, clampedDt)) {
this.collision.solveSwept(
this.chassis, this.collider,
this.prevPosition, this.prevQuaternion,
);
}
this.updateWheelRotation(clampedDt);
}
/** 把单个网格或列表收成可遍历数组。 */
private toColliderList(collider?: THREE.Mesh | THREE.Mesh[] | null): THREE.Mesh[] {
if (!collider) return [];
return Array.isArray(collider) ? collider : [collider];
}
/** 按索引取车轮,越界返回 undefined。 */
wheelAt(i: number): VehicleWheel | undefined {
return this.wheels[i];
}
// 把悬挂连接点、方向和转向后的轮轴变到世界空间
private updateWheelTransforms(): void {
const q = this.chassis.quaternion;
for (const wheel of this.wheels) {
wheel.hardPointWS.copy(wheel.connectionPoint).applyQuaternion(q).add(this.chassis.position);
wheel.directionWS.copy(wheel.direction).applyQuaternion(q);
this.steerAxis.copy(wheel.directionWS).negate();
this.steerQuat.setFromAxisAngle(this.steerAxis, wheel.steering);
wheel.axleWS.copy(wheel.axle).applyQuaternion(q).applyQuaternion(this.steerQuat);
}
}
// ==================== 悬挂与摩擦 ====================
// 沿悬挂方向射线检测接地
private raycastWheels(): void {
const colliders = this.collider;
for (const wheel of this.wheels) {
wheel.isInContact = false;
wheel.contactMesh = null;
const rayLen = wheel.restLength + wheel.maxSuspensionTravel + wheel.radius;
if (!colliders.length || rayLen <= 1e-6) {
this.setWheelAirborne(wheel);
continue;
}
const dirLen = wheel.directionWS.length();
if (dirLen < 1e-8) {
this.setWheelAirborne(wheel);
continue;
}
this.impulse.copy(wheel.directionWS).multiplyScalar(1 / dirLen);
this.raycaster.set(wheel.hardPointWS, this.impulse);
this.raycaster.near = 0;
this.raycaster.far = rayLen;
const hits = this.raycaster.intersectObjects(colliders, false);
const hit = hits[0];
if (!hit) {
this.setWheelAirborne(wheel);
continue;
}
wheel.isInContact = true;
wheel.contactMesh = (hit.object as THREE.Mesh).isMesh ? hit.object as THREE.Mesh : null;
wheel.contactPoint.copy(hit.point);
if (hit.face) {
this.normalMat.getNormalMatrix(hit.object.matrixWorld);
wheel.contactNormal.copy(hit.face.normal).applyMatrix3(this.normalMat).normalize();
} else {
wheel.contactNormal.copy(wheel.directionWS).negate().normalize();
}
if (wheel.contactNormal.dot(wheel.directionWS) > 0) wheel.contactNormal.negate();
// 物理长度夹在行程内;视觉长度跟射线
const rawLength = hit.distance - wheel.radius;
const minLen = Math.max(0, wheel.restLength - wheel.maxSuspensionTravel);
const maxLen = wheel.restLength + wheel.maxSuspensionTravel;
wheel.visualLength = rawLength;
wheel.suspensionLength = Math.min(maxLen, Math.max(minLen, rawLength));
const denom = wheel.contactNormal.dot(wheel.directionWS);
this.chassis.getVelocityAtPoint(wheel.contactPoint, this.vel);
const projVel = wheel.contactNormal.dot(this.vel);
if (denom >= -0.1) {
// 法线几乎与悬挂平行或反向,避免除零
wheel.suspensionRelativeVelocity = 0;
wheel.clippedInvContactDotSuspension = 10;
} else {
const inv = -1 / denom;
wheel.suspensionRelativeVelocity = projVel * inv;
wheel.clippedInvContactDotSuspension = inv;
}
}
}
// 离地时回到静止长度
private setWheelAirborne(wheel: VehicleWheel): void {
wheel.isInContact = false;
wheel.contactMesh = null;
wheel.suspensionLength = wheel.restLength;
wheel.visualLength = wheel.restLength;
wheel.suspensionRelativeVelocity = 0;
wheel.contactNormal.copy(wheel.directionWS).negate().normalize();
wheel.clippedInvContactDotSuspension = 1;
wheel.suspensionForce = 0;
}
// 弹簧 + 压缩/回弹阻尼
private solveSuspension(): void {
const chassisMass = this.chassis.mass();
for (const wheel of this.wheels) {
if (!wheel.isInContact) {
wheel.suspensionForce = 0;
continue;
}
const lengthDiff = wheel.restLength - wheel.suspensionLength;
let force = wheel.stiffness * lengthDiff * wheel.clippedInvContactDotSuspension;
const damping = wheel.suspensionRelativeVelocity < 0
? wheel.dampingCompression
: wheel.dampingRelaxation;
force -= damping * wheel.suspensionRelativeVelocity;
wheel.suspensionForce = Math.max(0, force * chassisMass); // 悬挂只推不拉
}
}
// 沿接触法线施加悬挂冲量;作用点用轮心,减少转向时接地点把车顶歪
private applySuspensionImpulses(dt: number): void {
for (const wheel of this.wheels) {
if (!wheel.isInContact) continue;
const force = Math.min(wheel.suspensionForce, wheel.maxSuspensionForce);
this.impulse.copy(wheel.contactNormal).multiplyScalar(force * dt);
this.chassis.applyImpulseAtPoint(this.impulse, wheel.hardPointWS);
}
}
// 侧向抓地、驱动/制动,超出摩擦椭圆则打滑缩放
private solveFriction(dt: number): void {
let wheelsOnGround = 0;
for (const wheel of this.wheels) {
wheel.sideImpulse = 0;
wheel.forwardImpulse = 0;
wheel.skidInfo = 1;
if (!wheel.isInContact) continue;
wheelsOnGround += 1;
wheel.sideWS.copy(wheel.axleWS);
const proj = wheel.sideWS.dot(wheel.contactNormal);
wheel.sideWS.addScaledVector(wheel.contactNormal, -proj);
if (wheel.sideWS.lengthSq() < 1e-8) continue;
wheel.sideWS.normalize();
wheel.forwardWS.copy(wheel.contactNormal).cross(wheel.sideWS);
if (wheel.forwardWS.lengthSq() < 1e-8) continue;
wheel.forwardWS.normalize();
// 侧向速度阻尼,抓地由 sideFrictionStiffness 缩放
this.chassis.getVelocityAtPoint(wheel.contactPoint, this.vel);
const sideSpeed = wheel.sideWS.dot(this.vel);
const mass = effectiveMass(
this.chassis, wheel.contactPoint, wheel.sideWS,
this.r, this.gcross, this.local, this.world, this.invQuat,
);
const dampFactor = Math.min(
1,
(1 - Math.exp(-SIDE_DAMPING_RATE * dt)) * wheel.sideFrictionStiffness,
);
wheel.sideImpulse = -dampFactor * sideSpeed * mass;
}
// 驱动或制动纵向冲量,并检测摩擦椭圆
let sliding = false;
for (const wheel of this.wheels) {
if (!wheel.isInContact) continue;
if (wheel.engineForce !== 0) {
wheel.forwardImpulse = wheel.engineForce * dt;
} else {
// 无驱动时用制动冲量刹住纵向滑动
const maxImpulse = wheel.brake;
this.chassis.getVelocityAtPoint(wheel.contactPoint, this.vel);
const fwdSpeed = wheel.forwardWS.dot(this.vel);
const mass = effectiveMass(
this.chassis, wheel.contactPoint, wheel.forwardWS,
this.r, this.gcross, this.local, this.world, this.invQuat,
);
const denom = Math.max(1, wheelsOnGround);
wheel.forwardImpulse = Math.max(-maxImpulse, Math.min(maxImpulse, -fwdSpeed * mass / denom));
}
const maxImp = wheel.suspensionForce * dt * wheel.frictionSlip;
const x = wheel.forwardImpulse * FWD_FACTOR;
const y = wheel.sideImpulse * SIDE_FACTOR;
const impulseSq = x * x + y * y;
const maxImpSq = maxImp * maxImp;
if (impulseSq > maxImpSq && impulseSq > 1e-12) {
sliding = true;
wheel.skidInfo *= maxImp / Math.sqrt(impulseSq);
}
}
if (sliding) {
// 超出椭圆的轮子按 skidInfo 同时缩小纵/侧向
for (const wheel of this.wheels) {
if (wheel.skidInfo < 1) {
wheel.forwardImpulse *= wheel.skidInfo;
wheel.sideImpulse *= wheel.skidInfo;
}
}
}
// 纵向冲量作用在轮心;侧向冲量作用在触地点,rollInfluence 收短侧倾力臂
this.upWS.set(0, 1, 0).applyQuaternion(this.chassis.quaternion);
for (const wheel of this.wheels) {
if (!wheel.isInContact) continue;
if (wheel.forwardImpulse !== 0) {
this.impulse.copy(wheel.forwardWS).multiplyScalar(wheel.forwardImpulse);
this.chassis.applyImpulseAtPoint(this.impulse, wheel.hardPointWS);
}
if (wheel.sideImpulse !== 0) {
this.impulse.copy(wheel.sideWS).multiplyScalar(wheel.sideImpulse);
this.sidePoint.copy(wheel.contactPoint);
this.r.subVectors(wheel.contactPoint, this.chassis.position);
this.sidePoint.addScaledVector(this.upWS, -this.upWS.dot(this.r) * (1 - wheel.rollInfluence));
this.chassis.applyImpulseAtPoint(this.impulse, this.sidePoint);
}
}
}
// 按接地点前向速度更新车轮自转
private updateWheelRotation(dt: number): void {
for (const wheel of this.wheels) {
if (wheel.isInContact) {
this.chassis.getVelocityAtPoint(wheel.hardPointWS, this.vel);
const proj = wheel.forwardWS.dot(this.vel);
wheel.deltaRotation = wheel.radius > 1e-6 ? -(proj * dt) / wheel.radius : 0;
wheel.rotation += wheel.deltaRotation;
} else {
wheel.rotation += wheel.deltaRotation;
}
wheel.deltaRotation *= 0.99;
}
}
}