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>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-15 21:27:03 +02:00
1 parent 780e246fe5
commit fb38652e7f
153 files changed
+42561 -4

No files matched your search

@@ -0,0 +1,485 @@
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;
}
}
}