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;
}
}
}
@@ -0,0 +1,373 @@
import * as THREE from "three";
import type { ContactManifold } from "../../collision/contacts/ContactManifold";
import { CONTACT_SKIN, contactSkinForExtent, type RawContact } from "../../collision/contacts/ContactPoint";
import { reduceContacts } from "../../collision/contacts/contactReducer";
import { closestPointOnTriangle } from "./vehicleMath";
import type { ImpulseBody } from "../../collision/solver/ImpulseBody";
import type { VehicleRigidBody } from "./VehicleRigidBody";
const MAX_RAW = 96; // 单帧原始接触上限
const CCD_SIZE_RATIO = 0.12; // 位移超过最小边长该比例时启用扫掠
/** detect 所需的 OBB 刚体(车辆底盘 / 动态盒共用)。 */
export type ObbMeshBody = ImpulseBody & {
halfExtents: THREE.Vector3;
};
/** 车身 OBB 与 BVH 三角网格:只收集接触,高速时做扫掠。 */
export class VehicleCollision {
private raw: RawContact[] = Array.from({ length: MAX_RAW }, () => ({
point: new THREE.Vector3(),
normal: new THREE.Vector3(),
penetration: 0,
}));
private rawCount = 0;
private manifolds: ContactManifold[] = [];
private toward = new THREE.Vector3();
private invMat = new THREE.Matrix4();
private worldMat = new THREE.Matrix4();
private normalMat = new THREE.Matrix3();
private localAABB = new THREE.Box3();
private worldAABB = new THREE.Box3();
private corner = new THREE.Vector3();
private prevCorner = new THREE.Vector3();
private axisX = new THREE.Vector3();
private axisY = new THREE.Vector3();
private axisZ = new THREE.Vector3();
private prevAxisX = new THREE.Vector3();
private prevAxisY = new THREE.Vector3();
private prevAxisZ = new THREE.Vector3();
private toiQuat = new THREE.Quaternion();
private localCenter = new THREE.Vector3();
private localAxisX = new THREE.Vector3();
private localAxisY = new THREE.Vector3();
private localAxisZ = new THREE.Vector3();
private triN = new THREE.Vector3();
private edge = new THREE.Vector3();
private testAxis = new THREE.Vector3();
private bestAxis = new THREE.Vector3();
private rangeA = { min: 0, max: 0 };
private rangeB = { min: 0, max: 0 };
private closest = new THREE.Vector3();
private ab = new THREE.Vector3();
private ac = new THREE.Vector3();
private ap = new THREE.Vector3();
private bp = new THREE.Vector3();
private cp = new THREE.Vector3();
private impulse = new THREE.Vector3();
private support = new THREE.Vector3();
private bodyHalf = new THREE.Vector3();
private raycaster = new THREE.Raycaster();
/** 当前 detect 按刚体尺寸缩放后的接触皮肤。 */
private contactSkin = CONTACT_SKIN;
/** 配置射线只取最近命中。 */
constructor() {
(this.raycaster as any).firstHitOnly = true;
}
/** 收集 OBB 与一组网格的接触流形。 */
detect(
body: ObbMeshBody,
colliders: THREE.Mesh[] | THREE.Mesh | null | undefined,
): ContactManifold[] {
this.rawCount = 0;
this.manifolds.length = 0;
const extent = Math.max(body.halfExtents.x, body.halfExtents.y, body.halfExtents.z);
this.contactSkin = contactSkinForExtent(extent);
for (const collider of this.toList(colliders)) {
const tree = (collider.geometry as any)?.boundsTree;
if (!tree) continue;
this.collectRawContacts(body, collider, tree);
}
if (this.rawCount === 0) return this.manifolds;
return reduceContacts(this.raw, this.rawCount, body, this.manifolds);
}
/** 本帧位移是否大到需要扫掠。 */
needsSwept(body: VehicleRigidBody, dt: number): boolean {
const hx = body.halfExtents.x;
const hy = body.halfExtents.y;
const hz = body.halfExtents.z;
const minSize = 2 * Math.min(hx, hy, hz);
const linTravel = body.linearVelocity.length() * dt;
const angTravel = body.angularVelocity.length() * Math.hypot(hx, hy, hz) * dt;
return linTravel + angTravel > minSize * CCD_SIZE_RATIO;
}
/** 高速穿模时按 TOI 回退位置和朝向。 */
solveSwept(
body: VehicleRigidBody,
colliders: THREE.Mesh[] | THREE.Mesh | null | undefined,
prevPosition: THREE.Vector3,
prevQuaternion: THREE.Quaternion,
): void {
const list = this.toList(colliders);
if (!list.length) return;
this.solveSweptCollision(body, list, prevPosition, prevQuaternion);
}
/** 对 8 个角点和中心扫过所有网格,按最早 TOI 回退。 */
private solveSweptCollision(
body: VehicleRigidBody,
colliders: THREE.Mesh[],
prevPosition: THREE.Vector3,
prevQuaternion: THREE.Quaternion,
): void {
this.bodyHalf.copy(body.halfExtents);
this.setAxes(prevQuaternion, this.prevAxisX, this.prevAxisY, this.prevAxisZ);
this.setAxes(body.quaternion, this.axisX, this.axisY, this.axisZ);
this.raycaster.near = 0;
let minToi = 1;
let hitNormal: THREE.Vector3 | null = null;
// 8 角点 + 中心沿本帧位移射线,取最早命中
for (let i = 0; i < 9; i++) {
if (i === 8) {
this.prevCorner.copy(prevPosition);
this.corner.copy(body.position);
} else {
const sx = (i & 1) ? 1 : -1;
const sy = (i & 2) ? 1 : -1;
const sz = (i & 4) ? 1 : -1;
this.cornerFrom(prevPosition, this.prevAxisX, this.prevAxisY, this.prevAxisZ, sx, sy, sz, this.prevCorner);
this.cornerFrom(body.position, this.axisX, this.axisY, this.axisZ, sx, sy, sz, this.corner);
}
this.impulse.subVectors(this.corner, this.prevCorner);
const len = this.impulse.length();
if (len < 1e-6) continue;
this.impulse.multiplyScalar(1 / len);
this.raycaster.set(this.prevCorner, this.impulse);
this.raycaster.far = len;
const hits = this.raycaster.intersectObjects(colliders, false);
if (!hits.length) continue;
const hit = hits[0];
const toi = hit.distance / len;
if (toi >= minToi || toi <= 1e-4) continue;
minToi = toi;
if (hit.face) {
this.normalMat.getNormalMatrix(hit.object.matrixWorld);
this.bestAxis.copy(hit.face.normal).applyMatrix3(this.normalMat).normalize();
if (this.bestAxis.dot(this.impulse) > 0) this.bestAxis.negate();
this.toward.copy(this.bestAxis);
hitNormal = this.toward;
}
}
if (minToi >= 1) return;
const t = minToi * 0.95; // 略提前停在命中前
body.position.lerpVectors(prevPosition, body.position, t);
this.toiQuat.slerpQuaternions(prevQuaternion, body.quaternion, t);
body.quaternion.copy(this.toiQuat);
// 去掉沿命中法线的入射速度
if (!hitNormal) return;
const vn = body.linearVelocity.dot(hitNormal);
if (vn < 0) body.linearVelocity.addScaledVector(hitNormal, -vn);
}
/** 把单个网格或列表收成可遍历数组。 */
private toList(
colliders: THREE.Mesh[] | THREE.Mesh | null | undefined,
): THREE.Mesh[] {
if (!colliders) return [];
return Array.isArray(colliders) ? colliders : [colliders];
}
/** 用 BVH shapecast 收集与 OBB 相交的三角。 */
private collectRawContacts(
body: ObbMeshBody,
collider: THREE.Mesh,
tree: { shapecast: (opts: any) => void },
): void {
this.setWorldAxes(body);
this.worldMat.copy(collider.matrixWorld);
this.invMat.copy(this.worldMat).invert();
this.normalMat.getNormalMatrix(this.worldMat);
// OBB 变到网格本地,再用 AABB 做 BVH 粗测
this.localCenter.copy(body.position).applyMatrix4(this.invMat);
this.localAxisX.copy(this.axisX).transformDirection(this.invMat);
this.localAxisY.copy(this.axisY).transformDirection(this.invMat);
this.localAxisZ.copy(this.axisZ).transformDirection(this.invMat);
this.computeOBBWorldAABB(body.position, this.worldAABB);
this.localAABB.copy(this.worldAABB).applyMatrix4(this.invMat);
const self = this;
tree.shapecast({
intersectsBounds: (box: THREE.Box3) => box.intersectsBox(self.localAABB),
intersectsTriangle: (tri: { a: THREE.Vector3; b: THREE.Vector3; c: THREE.Vector3 }) => {
self.testTriangle(body, tri.a, tri.b, tri.c);
},
});
}
/** SAT 分离则丢弃;否则用支撑点投影出接触。 */
private testTriangle(
body: ObbMeshBody,
a: THREE.Vector3,
b: THREE.Vector3,
c: THREE.Vector3,
): void {
this.ab.subVectors(b, a);
this.ac.subVectors(c, a);
this.triN.copy(this.ab).cross(this.ac);
if (this.triN.lengthSq() < 1e-12) return;
this.triN.normalize();
// 面法线、OBB 三轴、边×轴,任一分离则不相交
if (!this.tryAxis(this.triN, a, b, c)) return;
if (!this.tryAxis(this.localAxisX, a, b, c)) return;
if (!this.tryAxis(this.localAxisY, a, b, c)) return;
if (!this.tryAxis(this.localAxisZ, a, b, c)) return;
this.edge.subVectors(b, a);
if (!this.tryCrossAxes(this.edge, a, b, c)) return;
this.edge.subVectors(c, b);
if (!this.tryCrossAxes(this.edge, a, b, c)) return;
this.edge.subVectors(a, c);
if (!this.tryCrossAxes(this.edge, a, b, c)) return;
const radius =
this.bodyHalf.x * Math.abs(this.triN.dot(this.localAxisX)) +
this.bodyHalf.y * Math.abs(this.triN.dot(this.localAxisY)) +
this.bodyHalf.z * Math.abs(this.triN.dot(this.localAxisZ));
const plane = a.dot(this.triN);
const depth = plane - (this.localCenter.dot(this.triN) - radius);
if (depth <= -this.contactSkin) return;
// 沿法线取盒子支撑点,再投影到三角得到接触
this.support.copy(this.localCenter)
.addScaledVector(this.localAxisX, -this.bodyHalf.x * Math.sign(this.triN.dot(this.localAxisX) || 1))
.addScaledVector(this.localAxisY, -this.bodyHalf.y * Math.sign(this.triN.dot(this.localAxisY) || 1))
.addScaledVector(this.localAxisZ, -this.bodyHalf.z * Math.sign(this.triN.dot(this.localAxisZ) || 1));
closestPointOnTriangle(
this.support, a, b, c, this.closest,
this.ab, this.ac, this.ap, this.bp, this.cp,
);
this.addRawContact(this.closest, this.triN, depth, body);
}
/** 用三角边与 OBB 轴的叉积做分离轴测试。 */
private tryCrossAxes(
edge: THREE.Vector3,
a: THREE.Vector3,
b: THREE.Vector3,
c: THREE.Vector3,
): boolean {
if (!this.tryAxis(this.testAxis.copy(edge).cross(this.localAxisX), a, b, c)) return false;
if (!this.tryAxis(this.testAxis.copy(edge).cross(this.localAxisY), a, b, c)) return false;
if (!this.tryAxis(this.testAxis.copy(edge).cross(this.localAxisZ), a, b, c)) return false;
return true;
}
/** 沿一根轴投影 OBB 与三角,重叠则通过。 */
private tryAxis(
axis: THREE.Vector3,
a: THREE.Vector3,
b: THREE.Vector3,
c: THREE.Vector3,
): boolean {
const lenSq = axis.lengthSq();
if (lenSq < 1e-10) {
this.rangeB.max = Infinity;
return true; // 退化轴视为不分离
}
this.testAxis.copy(axis).multiplyScalar(1 / Math.sqrt(lenSq));
this.projectOBB(this.testAxis, this.rangeA);
const p0 = a.dot(this.testAxis);
const p1 = b.dot(this.testAxis);
const p2 = c.dot(this.testAxis);
this.rangeB.min = Math.min(p0, p1, p2);
const triMax = Math.max(p0, p1, p2);
if (this.rangeA.max + this.contactSkin < this.rangeB.min || this.rangeA.min - this.contactSkin > triMax) return false;
this.rangeB.max = Math.min(this.rangeA.max, triMax) - Math.max(this.rangeA.min, this.rangeB.min);
return true;
}
/** 把 OBB 投影到指定轴,写出 min/max。 */
private projectOBB(axis: THREE.Vector3, out: { min: number; max: number }): void {
const r =
this.bodyHalf.x * Math.abs(axis.dot(this.localAxisX)) +
this.bodyHalf.y * Math.abs(axis.dot(this.localAxisY)) +
this.bodyHalf.z * Math.abs(axis.dot(this.localAxisZ));
const c = this.localCenter.dot(axis);
out.min = c - r;
out.max = c + r;
}
/** 写入原始接触;槽满时替换最浅的一个。 */
private addRawContact(
localPoint: THREE.Vector3,
localNormal: THREE.Vector3,
depth: number,
body: ObbMeshBody,
): void {
let slot = this.rawCount;
if (slot === MAX_RAW) {
let shallow = 0;
for (let i = 1; i < MAX_RAW; i++) {
if (this.raw[i].penetration < this.raw[shallow].penetration) shallow = i;
}
if (depth <= this.raw[shallow].penetration) return;
slot = shallow;
} else {
this.rawCount += 1;
}
const contact = this.raw[slot];
contact.point.copy(localPoint).applyMatrix4(this.worldMat);
contact.normal.copy(localNormal).applyMatrix3(this.normalMat).normalize();
this.toward.subVectors(body.position, contact.point);
if (contact.normal.dot(this.toward) < 0) contact.normal.negate(); // 法线指向盒子
contact.penetration = depth;
}
/** 从朝向取出世界轴,并缓存半边长。 */
private setWorldAxes(body: ObbMeshBody): void {
this.setAxes(body.quaternion, this.axisX, this.axisY, this.axisZ);
this.bodyHalf.copy(body.halfExtents);
}
/** 由四元数写出三个正交轴。 */
private setAxes(
quat: THREE.Quaternion,
x: THREE.Vector3,
y: THREE.Vector3,
z: THREE.Vector3,
): void {
x.set(1, 0, 0).applyQuaternion(quat);
y.set(0, 1, 0).applyQuaternion(quat);
z.set(0, 0, 1).applyQuaternion(quat);
}
/** 由 8 个角点计算 OBB 世界 AABB。 */
private computeOBBWorldAABB(center: THREE.Vector3, out: THREE.Box3): void {
out.makeEmpty();
for (let i = 0; i < 8; i++) {
this.cornerFrom(
center, this.axisX, this.axisY, this.axisZ,
(i & 1) ? 1 : -1, (i & 2) ? 1 : -1, (i & 4) ? 1 : -1,
this.corner,
);
out.expandByPoint(this.corner);
}
out.expandByScalar(0.08); // 略放大,避免贴边漏三角
}
/** 按轴向符号组合出 OBB 角点。 */
private cornerFrom(
center: THREE.Vector3,
axisX: THREE.Vector3,
axisY: THREE.Vector3,
axisZ: THREE.Vector3,
sx: number,
sy: number,
sz: number,
out: THREE.Vector3,
): THREE.Vector3 {
return out.copy(center)
.addScaledVector(axisX, sx * this.bodyHalf.x)
.addScaledVector(axisY, sy * this.bodyHalf.y)
.addScaledVector(axisZ, sz * this.bodyHalf.z);
}
}
@@ -0,0 +1,174 @@
import * as THREE from "three";
import { integrateQuaternion } from "./vehicleMath";
export type VehicleRigidBodyOptions = {
position: THREE.Vector3; // 初始世界位置
mass: number; // 质量(kg)
halfExtents: THREE.Vector3; // 碰撞盒半边长
linearDamping?: number; // 线阻尼
angularDamping?: number; // 角阻尼
gravityScale?: number; // 重力缩放
};
const GRAVITY = 9.81; // 重力加速度
/** 车辆专用轻量刚体。 */
export class VehicleRigidBody {
position = new THREE.Vector3(); // 世界位置
quaternion = new THREE.Quaternion(); // 世界朝向
linearVelocity = new THREE.Vector3(); // 线速度
angularVelocity = new THREE.Vector3(); // 角速度
private _mass: number; // 质量
invMass: number; // 质量倒数
inertia = new THREE.Vector3(); // 局部惯量对角元
invInertia = new THREE.Vector3(); // 惯量倒数
linearDamping: number; // 线阻尼
angularDamping: number; // 角阻尼
gravityScale: number; // 重力缩放
halfExtents: THREE.Vector3; // 碰撞盒半边长
private _r = new THREE.Vector3();
private _torque = new THREE.Vector3();
private _local = new THREE.Vector3();
private _world = new THREE.Vector3();
private _invQuat = new THREE.Quaternion();
constructor(opts: VehicleRigidBodyOptions) {
this.position.copy(opts.position);
this.halfExtents = opts.halfExtents.clone();
this._mass = Math.max(1e-6, opts.mass);
this.invMass = 1 / this._mass;
this.linearDamping = opts.linearDamping ?? 0.05;
this.angularDamping = opts.angularDamping ?? 0.5;
this.gravityScale = opts.gravityScale ?? 1;
this.setCuboidInertia(this._mass, this.halfExtents);
}
/** 按长方体质量分布设置惯量。 */
setCuboidInertia(mass: number, half: THREE.Vector3): void {
const w = half.x * 2;
const h = half.y * 2;
const d = half.z * 2;
this.inertia.set(
mass / 12 * (h * h + d * d),
mass / 12 * (w * w + d * d),
mass / 12 * (w * w + h * h),
);
this.invInertia.set(
this.inertia.x > 1e-8 ? 1 / this.inertia.x : 0,
this.inertia.y > 1e-8 ? 1 / this.inertia.y : 0,
this.inertia.z > 1e-8 ? 1 / this.inertia.z : 0,
);
}
/** 读取线速度。 */
linvel(): THREE.Vector3 {
return this.linearVelocity;
}
/** 读取角速度。 */
angvel(): THREE.Vector3 {
return this.angularVelocity;
}
/** 设置线速度。 */
setLinvel(v: { x: number; y: number; z: number }): void {
this.linearVelocity.set(v.x, v.y, v.z);
}
/** 设置角速度。 */
setAngvel(v: { x: number; y: number; z: number }): void {
this.angularVelocity.set(v.x, v.y, v.z);
}
/** 读取世界位置。 */
translation(): THREE.Vector3 {
return this.position;
}
/** 读取世界朝向。 */
rotation(): THREE.Quaternion {
return this.quaternion;
}
/** 设置世界位置。 */
setTranslation(v: { x: number; y: number; z: number }): void {
this.position.set(v.x, v.y, v.z);
}
/** 设置世界朝向。 */
setRotation(q: { x: number; y: number; z: number; w: number }): void {
this.quaternion.set(q.x, q.y, q.z, q.w).normalize();
}
/** 读取质量。 */
mass(): number {
return this._mass;
}
/** 设置质量并按当前碰撞盒重算惯量。 */
setMass(mass: number): void {
this._mass = Math.max(1e-6, mass);
this.invMass = 1 / this._mass;
this.setCuboidInertia(this._mass, this.halfExtents);
}
/** 设置碰撞盒半边长并按当前质量重算惯量。 */
setHalfExtents(half: THREE.Vector3): void {
this.halfExtents.copy(half);
this.setCuboidInertia(this._mass, this.halfExtents);
}
/** 在质心施加冲量。 */
applyImpulse(impulse: THREE.Vector3): void {
this.linearVelocity.addScaledVector(impulse, this.invMass);
}
/** 在世界点施加冲量,同时产生角速度。 */
applyImpulseAtPoint(impulse: THREE.Vector3, contactPoint: THREE.Vector3): void {
this.linearVelocity.addScaledVector(impulse, this.invMass);
this._r.subVectors(contactPoint, this.position);
this.angularVelocity.add(this.angularDeltaFromImpulse(impulse, this._r));
}
// 将冲量转为世界角速度增量:ω += R I⁻¹ Rᵀ (r × J)
private angularDeltaFromImpulse(impulse: THREE.Vector3, r: THREE.Vector3): THREE.Vector3 {
this._torque.copy(r).cross(impulse);
this._invQuat.copy(this.quaternion).invert();
this._local.copy(this._torque).applyQuaternion(this._invQuat);
this._local.x *= this.invInertia.x;
this._local.y *= this.invInertia.y;
this._local.z *= this.invInertia.z;
return this._world.copy(this._local).applyQuaternion(this.quaternion);
}
/** 读取世界点处的刚体速度。 */
getVelocityAtPoint(point: THREE.Vector3, out: THREE.Vector3): THREE.Vector3 {
this._r.subVectors(point, this.position);
return out.copy(this.linearVelocity).add(this._world.copy(this.angularVelocity).cross(this._r)); // v + ω × r
}
/** 施加重力。 */
applyGravity(dt: number): void {
this.linearVelocity.y -= GRAVITY * this.gravityScale * dt;
}
/** 施加线/角阻尼。 */
applyDamping(dt: number): void {
// 指数阻尼,与步长无关的衰减
const lin = Math.exp(-this.linearDamping * dt);
const ang = Math.exp(-this.angularDamping * dt);
this.linearVelocity.multiplyScalar(lin);
this.angularVelocity.multiplyScalar(ang);
}
/** 用当前速度积分位置和朝向。 */
integrate(dt: number): void {
this.position.addScaledVector(this.linearVelocity, dt);
integrateQuaternion(this.quaternion, this.angularVelocity, dt);
}
}
@@ -0,0 +1,48 @@
import * as THREE from "three";
/** 单个车轮的悬挂、接地与轮胎力状态。 */
export class VehicleWheel {
connectionPoint = new THREE.Vector3(); // 悬挂与底盘的局部连接点
direction = new THREE.Vector3(0, -1, 0); // 悬挂方向(底盘局部)
axle = new THREE.Vector3(0, 0, -1); // 轮轴方向(底盘局部)
radius = 0.3; // 轮胎半径
restLength = 0.1; // 悬挂静止长度
suspensionLength = 0.1; // 当前悬挂长度(夹在行程内)
visualLength = 0.1; // 视觉悬挂长度,跟射线
maxSuspensionTravel = 0.1; // 最大行程
stiffness = 18; // 悬挂刚度(质量归一化)
dampingCompression = 2.1; // 压缩阻尼
dampingRelaxation = 2.5; // 回弹阻尼
maxSuspensionForce = 6000; // 最大悬挂力
steering = 0; // 转向角
engineForce = 0; // 驱动力
brake = 0; // 制动力
frictionSlip = 8; // 纵向抓地
sideFrictionStiffness = 1; // 侧向摩擦
rollInfluence = 0.12; // 侧向力对侧倾的影响
isInContact = false; // 是否接地
contactPoint = new THREE.Vector3(); // 接地点
contactNormal = new THREE.Vector3(0, 1, 0); // 接触法线
contactMesh: THREE.Mesh | null = null; // 本帧接地网格
suspensionForce = 0; // 本帧悬挂力
forwardImpulse = 0; // 纵向冲量
sideImpulse = 0; // 侧向冲量
rotation = 0; // 车轮自转角
deltaRotation = 0; // 本帧自转增量
hardPointWS = new THREE.Vector3(); // 连接点世界坐标
directionWS = new THREE.Vector3(0, -1, 0); // 悬挂方向(世界)
axleWS = new THREE.Vector3(0, 0, -1); // 轮轴(世界)
forwardWS = new THREE.Vector3(1, 0, 0); // 滚动前向(世界)
sideWS = new THREE.Vector3(0, 0, 1); // 侧向(世界)
clippedInvContactDotSuspension = 1; // 法线与悬挂方向夹角修正
suspensionRelativeVelocity = 0; // 沿悬挂方向的相对速度
skidInfo = 1; // 打滑系数,1 为未打滑
}
@@ -0,0 +1,115 @@
import * as THREE from "three";
const EPS = 1e-8; // 有效质量分母下限
/** 冲量公式所需的刚体字段。 */
type RigidBodyMassProps = {
position: THREE.Vector3;
quaternion: THREE.Quaternion;
invMass: number;
invInertia: THREE.Vector3;
};
/** 冲量分母:invMass + n · (I⁻¹ (r × n) × r) */
export function impulseDenominator(
body: RigidBodyMassProps,
point: THREE.Vector3,
normal: THREE.Vector3,
r: THREE.Vector3,
gcross: THREE.Vector3,
local: THREE.Vector3,
world: THREE.Vector3,
invQuat: THREE.Quaternion,
): number {
r.subVectors(point, body.position);
gcross.copy(r).cross(normal);
// 力矩变到局部惯量,再变回世界:I⁻¹ (r × n)
invQuat.copy(body.quaternion).invert();
local.copy(gcross).applyQuaternion(invQuat);
local.x *= body.invInertia.x;
local.y *= body.invInertia.y;
local.z *= body.invInertia.z;
world.copy(local).applyQuaternion(body.quaternion);
world.cross(r);
return body.invMass + normal.dot(world);
}
/** 沿方向约束的有效质量(冲量分母的倒数)。 */
export function effectiveMass(
body: RigidBodyMassProps,
point: THREE.Vector3,
normal: THREE.Vector3,
r: THREE.Vector3,
gcross: THREE.Vector3,
local: THREE.Vector3,
world: THREE.Vector3,
invQuat: THREE.Quaternion,
): number {
const denom = impulseDenominator(body, point, normal, r, gcross, local, world, invQuat);
return denom > EPS ? 1 / denom : 0;
}
/** 用世界角速度积分四元数:q += 0.5 * ω * q * dt */
export function integrateQuaternion(q: THREE.Quaternion, omega: THREE.Vector3, dt: number): void {
const halfDt = 0.5 * dt;
const qx = q.x, qy = q.y, qz = q.z, qw = q.w;
const wx = omega.x, wy = omega.y, wz = omega.z;
q.x += halfDt * (wx * qw + wy * qz - wz * qy);
q.y += halfDt * (wy * qw + wz * qx - wx * qz);
q.z += halfDt * (wz * qw + wx * qy - wy * qx);
q.w += halfDt * (-wx * qx - wy * qy - wz * qz);
q.normalize();
}
/** 三角形上距点 p 最近的点。 */
export function closestPointOnTriangle(
p: THREE.Vector3,
a: THREE.Vector3,
b: THREE.Vector3,
c: THREE.Vector3,
out: THREE.Vector3,
ab: THREE.Vector3,
ac: THREE.Vector3,
ap: THREE.Vector3,
bp: THREE.Vector3,
cp: THREE.Vector3,
): THREE.Vector3 {
ab.subVectors(b, a);
ac.subVectors(c, a);
ap.subVectors(p, a);
const d1 = ab.dot(ap);
const d2 = ac.dot(ap);
if (d1 <= 0 && d2 <= 0) return out.copy(a); // 顶点 A
bp.subVectors(p, b);
const d3 = ab.dot(bp);
const d4 = ac.dot(bp);
if (d3 >= 0 && d4 <= d3) return out.copy(b); // 顶点 B
const vc = d1 * d4 - d3 * d2;
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
const v = d1 / (d1 - d3);
return out.copy(a).addScaledVector(ab, v); // 边 AB
}
cp.subVectors(p, c);
const d5 = ab.dot(cp);
const d6 = ac.dot(cp);
if (d6 >= 0 && d5 <= d6) return out.copy(c); // 顶点 C
const vb = d5 * d2 - d1 * d6;
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
const w = d2 / (d2 - d6);
return out.copy(a).addScaledVector(ac, w); // 边 AC
}
const va = d3 * d6 - d5 * d4;
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) {
const w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
return out.copy(b).addScaledVector(ab.subVectors(c, b), w); // 边 BC
}
const denom = 1 / (va + vb + vc);
return out.copy(a).addScaledVector(ab, vb * denom).addScaledVector(ac, vc * denom); // 面内
}