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,182 @@
import type * as THREE from "three";
import { CollisionGroup } from "./groups";
/** 碰撞体运动类型。 */
export type MotionType = "static" | "kinematic" | "dynamic";
/**
* 碰撞形状。
* mesh 挂 BVH 供射线 / shapecast;box / sphere 用于动态刚体登记与解析碰撞。
*/
export type CollisionShape =
| { kind: "mesh"; mesh: THREE.Mesh }
| { kind: "box"; halfExtents: THREE.Vector3 }
| { kind: "sphere"; radius: number };
/** 世界中已登记的一条碰撞体。 */
export type CollisionCollider = {
id: number; // 稳定标识
motion: MotionType; // 运动类型
shape: CollisionShape; // 形状
groups: number; // 自己属于哪些业务组
mask: number; // 自己要撞哪些组
ready: boolean; // BVH / 资源是否可参与查询
userData?: unknown; // 业务侧挂载数据
};
/** 底层登记参数。未传的 groups / mask 按 motion 给默认值。 */
export type AddColliderOptions = {
motion: MotionType;
shape: CollisionShape;
groups?: number;
mask?: number;
ready?: boolean;
userData?: unknown;
};
/**
* 查询过滤。
* 传入完整 collider 时双方 groups/mask 都要匹配;只传 `{ mask }` 时单向过滤。
*/
export type MeshQueryFilter = CollisionCollider | { mask: number };
/** query / queryMeshes 的可选过滤。 */
export type MeshQueryOptions = {
skipIds?: number[]; // 额外排除的 id
kinds?: CollisionShape["kind"][]; // 只保留这些形状
motion?: MotionType | MotionType[]; // 只保留这些运动类型
};
/** 按运动类型给出默认业务分组与掩码。 */
function defaultBits(motion: MotionType): { groups: number; mask: number } {
switch (motion) {
case "static":
return {
groups: CollisionGroup.DEFAULT,
mask: CollisionGroup.DEBRIS | CollisionGroup.VEHICLE,
};
case "kinematic":
return {
groups: CollisionGroup.DEFAULT,
mask: CollisionGroup.DEBRIS | CollisionGroup.VEHICLE | CollisionGroup.CHARACTER,
};
case "dynamic":
return {
groups: CollisionGroup.DEBRIS,
mask: CollisionGroup.DEFAULT | CollisionGroup.VEHICLE | CollisionGroup.DEBRIS,
};
}
}
/** 是否为完整 collider(相对仅含 mask 的单向查询)。 */
function isCollider(filter: MeshQueryFilter): filter is CollisionCollider {
return "id" in filter && "motion" in filter && "groups" in filter;
}
/**
* 碰撞体注册表。
* 人物仍用胶囊推出,车辆仍用射线 / OBB;这里只统一「谁登记了、谁和谁该测」。
*/
export class CollisionWorld {
private nextId = 1; // 递增 id
private readonly colliders = new Map<number, CollisionCollider>(); // 已登记碰撞体
/** 登记一条碰撞体,返回带 id 的条目。 */
add(options: AddColliderOptions): CollisionCollider {
const bits = defaultBits(options.motion);
const entry: CollisionCollider = {
id: this.nextId++,
motion: options.motion,
shape: options.shape,
groups: options.groups ?? bits.groups,
mask: options.mask ?? bits.mask,
ready: options.ready ?? true,
userData: options.userData,
};
this.colliders.set(entry.id, entry);
return entry;
}
/** 按 id 取碰撞体,不存在则返回 undefined。 */
get(id: number): CollisionCollider | undefined {
return this.colliders.get(id);
}
/** 注销碰撞体。 */
remove(id: number): void {
this.colliders.delete(id);
}
/** 标记 BVH / 资源是否可参与查询。 */
setReady(id: number, ready: boolean): void {
const entry = this.colliders.get(id);
if (entry) entry.ready = ready;
}
/** 覆盖碰撞掩码。 */
setMask(id: number, mask: number): void {
const entry = this.colliders.get(id);
if (entry) entry.mask = mask;
}
/** 覆盖业务分组。 */
setGroups(id: number, groups: number): void {
const entry = this.colliders.get(id);
if (entry) entry.groups = groups;
}
/** 覆盖 userData。 */
setUserData(id: number, userData: unknown): void {
const entry = this.colliders.get(id);
if (entry) entry.userData = userData;
}
/**
* 查询可参与检测的碰撞体。
* 完整 collider:双向 groups/mask;仅 mask:单向。自动跳过未 ready 与自身 id。
*/
query(filter: MeshQueryFilter, options?: MeshQueryOptions): CollisionCollider[] {
const skip = new Set(options?.skipIds);
const self = isCollider(filter) ? filter : null;
if (self) skip.add(self.id);
const mask = filter.mask;
const kinds = options?.kinds;
const motions = options?.motion
? (Array.isArray(options.motion) ? options.motion : [options.motion])
: null;
const out: CollisionCollider[] = [];
for (const col of this.colliders.values()) {
if (!col.ready || skip.has(col.id)) continue;
if (motions && !motions.includes(col.motion)) continue;
if (kinds && !kinds.includes(col.shape.kind)) continue;
if ((col.groups & mask) === 0) continue;
if (self && (self.groups & col.mask) === 0) continue;
out.push(col);
}
return out;
}
/** 查询可做 BVH 射线 / shapecast 的网格。 */
queryMeshes(filter: MeshQueryFilter, options?: MeshQueryOptions): THREE.Mesh[] {
const meshes: THREE.Mesh[] = [];
for (const col of this.query(filter, options)) {
if (col.shape.kind === "mesh") meshes.push(col.shape.mesh);
}
return meshes;
}
/** 按运动类型列出 id(供 clearColliders)。 */
idsByMotion(motion?: MotionType): number[] {
const out: number[] = [];
for (const col of this.colliders.values()) {
if (motion && col.motion !== motion) continue;
out.push(col.id);
}
return out;
}
/** 清空全部登记。 */
clear(): void {
this.colliders.clear();
}
}
@@ -0,0 +1,187 @@
import * as THREE from "three";
import { integrateQuaternion } from "../utils/vehiclePhysics/vehicleMath";
/** 冲量唤醒:质心线速度增量 ‖Δv‖ = ‖J‖·invMass 超过该值则醒(世界单位/秒)。 */
const WAKE_DELTA_V = 1e-4;
const WAKE_DELTA_V_SQ = WAKE_DELTA_V * WAKE_DELTA_V;
/** 动态刚体形状判别。 */
export type DynamicBodyKind = "sphere" | "box";
/** 动态刚体材质 / 初态可选字段(各形状 options 共用)。 */
export type DynamicBodyMaterialOptions = {
/** 密度,质量 = 体积 × 密度;默认见 DYNAMIC_BODY_DEFAULTS。 */
density?: number;
/** 弹性,0 不反弹,1 完全弹;默认见 DYNAMIC_BODY_DEFAULTS。 */
restitution?: number;
/** 摩擦;默认见 DYNAMIC_BODY_DEFAULTS。 */
friction?: number;
/** 线阻尼,越大滑行越快停;默认见 DYNAMIC_BODY_DEFAULTS。 */
linearDamping?: number;
/** 角阻尼,越大旋转越快停;默认见 DYNAMIC_BODY_DEFAULTS。 */
angularDamping?: number;
/** 重力,默认沿用控制器重力。 */
gravity?: number;
/** 初速度。 */
velocity?: THREE.Vector3;
/** 初角速度。 */
angularVelocity?: THREE.Vector3;
/** 视觉网格。 */
mesh?: THREE.Mesh;
};
/** 子类构造时传入的已解析材质与视觉(缺省已在上层填好)。 */
export type DynamicBodyBaseInit = {
density: number; // 密度
mass: number; // 质量(体积 × 密度)
restitution: number; // 弹性
gravity: number; // 重力加速度
position: THREE.Vector3; // 世界位置
velocity: THREE.Vector3; // 线速度
angularVelocity?: THREE.Vector3; // 角速度
linearDamping: number; // 线阻尼
angularDamping: number; // 角阻尼
friction: number; // 摩擦
mesh: THREE.Mesh; // 视觉网格
debugMesh: THREE.Mesh; // 碰撞线框
};
/**
* 动态刚体基类(ImpulseBody + 材质)。
* 形状相关惯量 / 特征尺寸由子类提供;窄相算法按 kind 分派。
*/
export abstract class DynamicBody {
abstract readonly kind: DynamicBodyKind; // 形状判别
density: number; // 密度
/** 由 density × 体积得到,供冲量求解使用。 */
mass: number;
restitution: number; // 弹性 [0, 1]
gravity: number; // 重力加速度(通常为负)
position = new THREE.Vector3(); // 世界位置
quaternion = new THREE.Quaternion(); // 世界朝向
/** 线速度(保留 velocity 命名以兼容现有调用)。 */
velocity = new THREE.Vector3();
angularVelocity = new THREE.Vector3(); // 角速度
invMass = 0; // 质量倒数
invInertia = new THREE.Vector3(); // 局部惯量倒数对角元
linearDamping: number; // 线阻尼
angularDamping: number; // 角阻尼
friction: number; // 碰撞摩擦
mesh: THREE.Mesh; // 视觉网格
/** 碰撞形状线框,由 setDynamicBodyDebug 显隐。 */
debugMesh: THREE.Mesh;
contactMesh: THREE.Mesh | null = null; // 本帧接触的网格(平台携带用)
colliderId = 0; // CollisionWorld 登记 id
/** 休眠中则跳过积分与窄相。 */
sleeping = false;
/** 是否允许进入休眠。 */
canSleep = true;
/** 连续低于休眠阈值的累计时间(秒)。 */
sleepTimer = 0;
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();
/** 写入材质、位姿与视觉引用,并计算 invMass。 */
protected constructor(opts: DynamicBodyBaseInit) {
this.density = Math.max(1e-8, opts.density);
this.mass = Math.max(1e-6, opts.mass);
this.restitution = Math.max(0, Math.min(1, opts.restitution));
this.gravity = opts.gravity;
this.position.copy(opts.position);
this.velocity.copy(opts.velocity);
if (opts.angularVelocity) this.angularVelocity.copy(opts.angularVelocity);
this.linearDamping = opts.linearDamping;
this.angularDamping = opts.angularDamping;
this.friction = opts.friction;
this.mesh = opts.mesh;
this.debugMesh = opts.debugMesh;
this.invMass = 1 / this.mass;
}
/** 用于速度钳制 / 携带阈值的特征半尺寸。 */
abstract characteristicExtent(): number;
/** 唤醒刚体。 */
wakeUp(): void {
if (!this.sleeping) return;
this.sleeping = false;
this.sleepTimer = 0;
}
/** 进入休眠并清零速度。 */
putToSleep(): void {
if (!this.canSleep) return;
this.sleeping = true;
this.sleepTimer = 0;
this.velocity.set(0, 0, 0);
this.angularVelocity.set(0, 0, 0);
this.contactMesh = null;
}
/** 线速度 / 角速度是否低于给定阈值。 */
isRestingCandidate(linearThreshold: number, angularThreshold: number): boolean {
return (
this.velocity.lengthSq() <= linearThreshold * linearThreshold
&& this.angularVelocity.lengthSq() <= angularThreshold * angularThreshold
);
}
/** 读取世界点处的刚体速度:v + ω × r。 */
getVelocityAtPoint(point: THREE.Vector3, out: THREE.Vector3): THREE.Vector3 {
this._r.subVectors(point, this.position);
return out.copy(this.velocity).add(this._world.copy(this.angularVelocity).cross(this._r));
}
/**
* 休眠唤醒:速度增量 ‖J‖·invMass 超过阈值则醒。
*/
private wakeIfImpulseMeaningful(impulse: THREE.Vector3): void {
if (!this.sleeping) return;
const inv = this.invMass;
if (impulse.lengthSq() * inv * inv > WAKE_DELTA_V_SQ) this.wakeUp();
}
/** 在质心施加冲量。 */
applyImpulse(impulse: THREE.Vector3): void {
this.wakeIfImpulseMeaningful(impulse);
this.velocity.addScaledVector(impulse, this.invMass);
}
/** 在世界点施加冲量,同时产生角速度。 */
applyImpulseAtPoint(impulse: THREE.Vector3, contactPoint: THREE.Vector3): void {
this.wakeIfImpulseMeaningful(impulse);
this.velocity.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);
}
/** 指数线 / 角阻尼。 */
applyDamping(dt: number): void {
const lin = Math.exp(-this.linearDamping * dt);
const ang = Math.exp(-this.angularDamping * dt);
this.velocity.multiplyScalar(lin);
this.angularVelocity.multiplyScalar(ang);
}
/** 用当前速度积分位置和朝向。 */
integrate(dt: number): void {
this.position.addScaledVector(this.velocity, dt);
integrateQuaternion(this.quaternion, this.angularVelocity, dt);
}
}
@@ -0,0 +1,171 @@
import * as THREE from "three";
import { DynamicBody } from "./DynamicBody";
import type { CollisionTemps } from "../utils/capsuleCollision";
/**
* 动态盒刚体。
* 惯量按实心长方体;与网格的接触冲量走 ContactImpulseSolver,盒–盒走 resolveObbObb。
*/
export class DynamicBoxBody extends DynamicBody {
readonly kind = "box" as const; // 形状判别
halfExtents = new THREE.Vector3(); // 碰撞盒半边长
/** 按体积 × 密度算质量,并写入长方体惯量。 */
constructor(opts: {
halfExtents: THREE.Vector3;
density: number;
restitution: number;
gravity: number;
position: THREE.Vector3;
quaternion?: THREE.Quaternion;
velocity: THREE.Vector3;
angularVelocity?: THREE.Vector3;
linearDamping: number;
angularDamping: number;
friction: number;
mesh: THREE.Mesh;
debugMesh: THREE.Mesh;
}) {
const hx = Math.max(1e-4, opts.halfExtents.x);
const hy = Math.max(1e-4, opts.halfExtents.y);
const hz = Math.max(1e-4, opts.halfExtents.z);
const volume = 8 * hx * hy * hz;
super({
density: opts.density,
mass: volume * Math.max(1e-8, opts.density),
restitution: opts.restitution,
gravity: opts.gravity,
position: opts.position,
velocity: opts.velocity,
angularVelocity: opts.angularVelocity,
linearDamping: opts.linearDamping,
angularDamping: opts.angularDamping,
friction: opts.friction,
mesh: opts.mesh,
debugMesh: opts.debugMesh,
});
this.halfExtents.set(hx, hy, hz);
if (opts.quaternion) this.quaternion.copy(opts.quaternion);
this.setCuboidInertia();
}
/** 特征半尺寸取三边最大值。 */
characteristicExtent(): number {
return Math.max(this.halfExtents.x, this.halfExtents.y, this.halfExtents.z);
}
/** 实心长方体惯量 I = m/12 (h²+d²) 等,并写 invInertia。 */
setCuboidInertia(): void {
const m = this.mass;
const w = this.halfExtents.x * 2;
const h = this.halfExtents.y * 2;
const d = this.halfExtents.z * 2;
const ix = Math.max(1e-12, m / 12 * (h * h + d * d));
const iy = Math.max(1e-12, m / 12 * (w * w + d * d));
const iz = Math.max(1e-12, m / 12 * (w * w + h * h));
this.invInertia.set(1 / ix, 1 / iy, 1 / iz);
}
}
/** 类型收窄:动态盒。 */
export function isBoxBody(body: DynamicBody): body is DynamicBoxBody {
return body.kind === "box";
}
const CAPSULE_OBB_SAMPLES = 8; // 胶囊轴采样点数(含端点)
const _invQuat = new THREE.Quaternion();
const _local = new THREE.Vector3();
const _sample = new THREE.Vector3();
const _bestOnSeg = new THREE.Vector3();
const _bestDirLocal = new THREE.Vector3();
const _candDir = new THREE.Vector3();
/**
* 胶囊 vs OBB:沿胶囊段采样,取到 AABB 的最近距离。
* 命中时 temps.closestTri 为**胶囊指向刚体**的单位法线(与 capsuleSphereOverlap 一致),
* temps.closestSeg 为段上点,返回穿透深度。
*/
export function capsuleObbOverlap(
capsule: THREE.Object3D,
capsuleInfo: { segment: THREE.Line3; radius: number; dynamicsSegment?: THREE.Line3 },
boxPos: THREE.Vector3,
boxQuat: THREE.Quaternion,
half: THREE.Vector3,
temps: CollisionTemps,
): number {
const segment = capsuleInfo.dynamicsSegment ?? capsuleInfo.segment;
temps.localSeg.start.copy(segment.start).applyMatrix4(capsule.matrixWorld);
temps.localSeg.end.copy(segment.end).applyMatrix4(capsule.matrixWorld);
_invQuat.copy(boxQuat).invert();
const hx = half.x;
const hy = half.y;
const hz = half.z;
let bestDist = Infinity;
let insideAxis = -1; // 0/1/2 表示在盒内时选的推出轴
let insideSign = 1;
let insideDepth = 0;
for (let i = 0; i <= CAPSULE_OBB_SAMPLES; i++) {
const t = i / CAPSULE_OBB_SAMPLES;
_sample.lerpVectors(temps.localSeg.start, temps.localSeg.end, t);
_local.copy(_sample).sub(boxPos).applyQuaternion(_invQuat);
const inside =
Math.abs(_local.x) <= hx &&
Math.abs(_local.y) <= hy &&
Math.abs(_local.z) <= hz;
if (inside) {
const dx = hx - Math.abs(_local.x);
const dy = hy - Math.abs(_local.y);
const dz = hz - Math.abs(_local.z);
const depth = capsuleInfo.radius + Math.min(dx, dy, dz);
if (depth > insideDepth) {
insideDepth = depth;
_bestOnSeg.copy(_sample);
// 取穿透最小的面,法线先取盒心→外侧(稍后翻成胶囊→盒)
if (dx <= dy && dx <= dz) {
insideAxis = 0;
insideSign = Math.sign(_local.x || 1);
} else if (dy <= dz) {
insideAxis = 1;
insideSign = Math.sign(_local.y || 1);
} else {
insideAxis = 2;
insideSign = Math.sign(_local.z || 1);
}
}
continue;
}
const cx = Math.min(hx, Math.max(-hx, _local.x));
const cy = Math.min(hy, Math.max(-hy, _local.y));
const cz = Math.min(hz, Math.max(-hz, _local.z));
// 盒表面 → 采样点(盒→胶囊);返回前取反
_candDir.set(_local.x - cx, _local.y - cy, _local.z - cz);
const dist = _candDir.length();
if (dist >= bestDist) continue;
bestDist = dist;
_bestOnSeg.copy(_sample);
if (dist > 1e-8) _bestDirLocal.copy(_candDir).multiplyScalar(1 / dist);
else _bestDirLocal.set(0, 1, 0);
}
if (insideDepth > 0) {
temps.closestSeg.copy(_bestOnSeg);
if (insideAxis === 0) temps.closestTri.set(insideSign, 0, 0);
else if (insideAxis === 1) temps.closestTri.set(0, insideSign, 0);
else temps.closestTri.set(0, 0, insideSign);
// 盒外法线 → 翻成胶囊指向刚体(进入盒的方向)
temps.closestTri.applyQuaternion(boxQuat).negate();
return insideDepth;
}
if (bestDist >= capsuleInfo.radius) return 0;
temps.closestSeg.copy(_bestOnSeg);
// 盒→胶囊 取反 → 胶囊→盒
temps.closestTri.copy(_bestDirLocal).applyQuaternion(boxQuat).negate();
return capsuleInfo.radius - bestDist;
}
@@ -0,0 +1,247 @@
import * as THREE from "three";
import { contactSkinForExtent, type RawContact } from "./contacts/ContactPoint";
import { DynamicBody } from "./DynamicBody";
/**
* 动态球刚体。
* 球惯量 I = ⅖ m r²;与网格的接触冲量走 ContactImpulseSolver。
*/
export class DynamicSphereBody extends DynamicBody {
readonly kind = "sphere" as const; // 形状判别
radius: number; // 碰撞半径
/** 按体积 × 密度算质量,并写入球惯量。 */
constructor(opts: {
radius: number;
density: number;
restitution: number;
gravity: number;
position: THREE.Vector3;
velocity: THREE.Vector3;
angularVelocity?: THREE.Vector3;
linearDamping: number;
angularDamping: number;
friction: number;
mesh: THREE.Mesh;
debugMesh: THREE.Mesh;
}) {
const volume = (4 / 3) * Math.PI * opts.radius * opts.radius * opts.radius;
super({
density: opts.density,
mass: volume * Math.max(1e-8, opts.density),
restitution: opts.restitution,
gravity: opts.gravity,
position: opts.position,
velocity: opts.velocity,
angularVelocity: opts.angularVelocity,
linearDamping: opts.linearDamping,
angularDamping: opts.angularDamping,
friction: opts.friction,
mesh: opts.mesh,
debugMesh: opts.debugMesh,
});
this.radius = opts.radius;
this.setSphereInertia();
}
/** 特征半尺寸即半径。 */
characteristicExtent(): number {
return this.radius;
}
/** 实心球惯量 I = ⅖ m r²(对角),并写 invInertia。 */
setSphereInertia(): void {
// 小尺度物体 I 可低于旧地板 1e-8;置 0 会导致无法滚动、地面摩擦过粘
const i = Math.max(1e-12, 0.4 * this.mass * this.radius * this.radius);
const inv = 1 / i;
this.invInertia.set(inv, inv, inv);
}
}
/** 类型收窄:动态球。 */
export function isSphereBody(body: DynamicBody): body is DynamicSphereBody {
return body.kind === "sphere";
}
/** 球窄相预分配临时量(避免每帧 new)。 */
export type SphereCollisionTemps = {
invMat: THREE.Matrix4; // mesh.matrixWorld 的逆
normalMat: THREE.Matrix3; // 法线变换矩阵
invQuat: THREE.Quaternion; // 盒朝向的逆
localSphere: THREE.Sphere; // 网格本地空间中的查询球
closest: THREE.Vector3; // 三角面 / 盒上最近点(本地)
worldClosest: THREE.Vector3; // 最近点世界坐标
offset: THREE.Vector3; // 球心到最近点的偏移
before: THREE.Vector3; // 推出前位置备份
normal: THREE.Vector3; // 接触法线(由对面指向球)
local: THREE.Vector3; // 盒局部坐标
contact: THREE.Vector3; // 世界接触点
boxVel: THREE.Vector3; // 盒上接触点速度
impulse: THREE.Vector3; // 冲量缓冲
};
/** 创建球碰撞临时量。 */
export function createSphereCollisionTemps(): SphereCollisionTemps {
return {
invMat: new THREE.Matrix4(),
normalMat: new THREE.Matrix3(),
invQuat: new THREE.Quaternion(),
localSphere: new THREE.Sphere(),
closest: new THREE.Vector3(),
worldClosest: new THREE.Vector3(),
offset: new THREE.Vector3(),
before: new THREE.Vector3(),
normal: new THREE.Vector3(),
local: new THREE.Vector3(),
contact: new THREE.Vector3(),
boxVel: new THREE.Vector3(),
impulse: new THREE.Vector3(),
};
}
/**
* 收集球 vs 单个 BVH 网格的原始接触(不移动球心)。
* 法线由表面指向球心;penetration = radius - 距离;返回写入后的 rawCount。
*/
export function collectSphereVsMeshContacts(
position: THREE.Vector3,
radius: number,
mesh: THREE.Mesh,
temps: SphereCollisionTemps,
raw: RawContact[],
rawCount: number,
maxRaw: number,
): number {
const tree = (mesh.geometry as any)?.boundsTree;
if (!tree || rawCount >= maxRaw) return rawCount;
// 球变到网格本地,均匀缩放用 scale.x 近似
temps.invMat.copy(mesh.matrixWorld).invert();
temps.localSphere.center.copy(position).applyMatrix4(temps.invMat);
const scale = mesh.scale.x || 1;
temps.localSphere.radius = radius / scale;
temps.normalMat.getNormalMatrix(mesh.matrixWorld);
tree.shapecast({
intersectsBounds: (box: THREE.Box3) => box.intersectsSphere(temps.localSphere),
intersectsTriangle: (tri: any) => {
if (rawCount >= maxRaw) return true; // 触达上限则提前结束遍历
tri.closestPointToPoint(temps.localSphere.center, temps.closest);
temps.offset.subVectors(temps.localSphere.center, temps.closest);
const dist = temps.offset.length();
const penetration = temps.localSphere.radius - dist;
// 允许轻微皮肤重叠(按球半径缩放,本地空间与 mesh.scale 对齐)
const localSkin = contactSkinForExtent(radius) / Math.max(scale, 1e-8);
if (penetration < -localSkin) return;
if (dist > 1e-8) temps.normal.copy(temps.offset).multiplyScalar(1 / dist);
else temps.normal.set(0, 1, 0); // 球心几乎落在面上时给默认上法线
temps.normal.applyMatrix3(temps.normalMat).normalize();
temps.worldClosest.copy(temps.closest).applyMatrix4(mesh.matrixWorld);
const slot = raw[rawCount++];
slot.point.copy(temps.worldClosest);
slot.normal.copy(temps.normal);
slot.penetration = penetration * scale; // 穿透深度还原到世界尺度
},
boundsTraverseOrder: (box: THREE.Box3) => box.distanceToPoint(temps.localSphere.center) - temps.localSphere.radius,
});
return rawCount;
}
/**
* 球 vs OBB:重叠则推出球并写 temps.normal / contact。
* 法线由盒指向球;会就地修改 position。
*/
export function collideSphereVsObb(
position: THREE.Vector3,
radius: number,
boxPos: THREE.Vector3,
boxQuat: THREE.Quaternion,
half: THREE.Vector3,
temps: SphereCollisionTemps,
): boolean {
// 球心变到盒局部
temps.invQuat.copy(boxQuat).invert();
temps.local.copy(position).sub(boxPos).applyQuaternion(temps.invQuat);
const hx = half.x;
const hy = half.y;
const hz = half.z;
const inside =
Math.abs(temps.local.x) <= hx &&
Math.abs(temps.local.y) <= hy &&
Math.abs(temps.local.z) <= hz;
if (inside) {
// 球心已在盒内:沿穿透最小的轴推出到表面外 radius
const dx = hx - Math.abs(temps.local.x);
const dy = hy - Math.abs(temps.local.y);
const dz = hz - Math.abs(temps.local.z);
if (dx <= dy && dx <= dz) {
temps.local.x = Math.sign(temps.local.x || 1) * (hx + radius);
temps.normal.set(Math.sign(temps.local.x), 0, 0);
} else if (dy <= dz) {
temps.local.y = Math.sign(temps.local.y || 1) * (hy + radius);
temps.normal.set(0, Math.sign(temps.local.y), 0);
} else {
temps.local.z = Math.sign(temps.local.z || 1) * (hz + radius);
temps.normal.set(0, 0, Math.sign(temps.local.z));
}
temps.normal.applyQuaternion(boxQuat);
position.copy(temps.local).applyQuaternion(boxQuat).add(boxPos);
temps.contact.copy(position).addScaledVector(temps.normal, -radius);
return true;
}
// 盒外:钳到盒表面最近点,若距球心 < radius 则推出
temps.closest.set(
Math.min(hx, Math.max(-hx, temps.local.x)),
Math.min(hy, Math.max(-hy, temps.local.y)),
Math.min(hz, Math.max(-hz, temps.local.z)),
);
temps.offset.subVectors(temps.local, temps.closest);
const dist = temps.offset.length();
if (dist >= radius || dist < 1e-8) return false;
temps.offset.multiplyScalar(1 / dist);
temps.local.addScaledVector(temps.offset, radius - dist);
temps.normal.copy(temps.offset).applyQuaternion(boxQuat);
position.copy(temps.local).applyQuaternion(boxQuat).add(boxPos);
temps.contact.copy(temps.closest).applyQuaternion(boxQuat).add(boxPos);
return true;
}
/**
* 两球按质量推出并交换法向速度。
* 暂不走双刚体 ContactImpulseSolver(无摩擦 / 无角冲量)。
*/
export function resolveSphereSphere(
a: DynamicSphereBody,
b: DynamicSphereBody,
temps: SphereCollisionTemps,
restitution: number,
): void {
temps.offset.subVectors(a.position, b.position);
const dist = temps.offset.length();
const minDist = a.radius + b.radius;
if (dist >= minDist) return;
if (dist > 1e-8) temps.normal.copy(temps.offset).multiplyScalar(1 / dist);
else temps.normal.set(0, 1, 0); // 完全重合时给默认法线
// 位置分离(按 invMass 分配)
const depth = minDist - dist;
const invA = a.invMass;
const invB = b.invMass;
const invSum = invA + invB;
a.position.addScaledVector(temps.normal, depth * (invA / invSum));
b.position.addScaledVector(temps.normal, -depth * (invB / invSum));
// 仅处理接近中的法向相对速度
const rel = a.velocity.dot(temps.normal) - b.velocity.dot(temps.normal);
if (rel >= 0) return;
const impulse = -(1 + restitution) * rel / invSum;
a.velocity.addScaledVector(temps.normal, impulse * invA);
b.velocity.addScaledVector(temps.normal, -impulse * invB);
}
@@ -0,0 +1,242 @@
import * as THREE from "three";
import { MeshBVH, acceleratedRaycast } from "three-mesh-bvh";
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
import type { BvhWorkerPool } from "../utils/BvhWorkerPool";
/**
* 碰撞网格构建工具。
*/
/** 调试辅助物及其子级不应被烘焙进 mesh collider。 */
function isExcludedFromCollider(object: THREE.Object3D): boolean {
let current: THREE.Object3D | null = object;
while (current) {
if (current.userData.excludeFromCollider === true) return true;
current = current.parent;
}
return false;
}
/** 校验并补全合并所需属性(position / normal / uv);含 NaN 或顶点数不足时返回 null。 */
export function ensureAttributesMinimal(geom: THREE.BufferGeometry): THREE.BufferGeometry | null {
if (!geom.attributes.position) return null;
const position = geom.attributes.position.array;
for (let i = 0; i < position.length; i++) {
if (!Number.isFinite(position[i])) return null;
}
if (geom.attributes.position.count < 3) return null;
if (!geom.attributes.normal) geom.computeVertexNormals();
if (!geom.attributes.uv) {
const count = geom.attributes.position.count;
geom.setAttribute("uv", new THREE.BufferAttribute(new Float32Array(count * 2), 2));
}
return geom;
}
/**
* 统一一组几何的属性布局,便于 BufferGeometryUtils.mergeGeometries。
* 去掉非必需属性;冲突项删除;缺失项用零填充。
*/
export function unifiedAttribute(collected: THREE.BufferGeometry[]): THREE.BufferGeometry[] {
type AttrMeta = { itemSize: number; arrayCtor: any; examples: number; normalized: boolean };
const attrMap = new Map<string, AttrMeta>();
const attrConflict = new Set<string>();
const required = new Set(["position", "normal", "uv"]);
for (const g of collected)
for (const name of Object.keys(g.attributes))
if (!required.has(name)) g.deleteAttribute(name);
for (const g of collected) {
for (const name of Object.keys(g.attributes)) {
const attr = g.attributes[name] as THREE.BufferAttribute;
const ctor = (attr.array as any).constructor;
if (!attrMap.has(name)) {
attrMap.set(name, { itemSize: attr.itemSize, arrayCtor: ctor, examples: 1, normalized: attr.normalized });
} else {
const m = attrMap.get(name)!;
if (m.itemSize !== attr.itemSize || m.arrayCtor !== ctor || m.normalized !== attr.normalized) attrConflict.add(name);
else m.examples++;
}
}
}
for (const name of attrConflict) {
for (const g of collected) if (g.attributes[name]) g.deleteAttribute(name);
attrMap.delete(name);
}
for (const [name, meta] of attrMap) {
for (const g of collected) {
if (!g.attributes[name]) {
const count = g.attributes.position.count;
g.setAttribute(name, new THREE.BufferAttribute(new meta.arrayCtor(count * meta.itemSize), meta.itemSize, meta.normalized));
}
}
}
return collected;
}
/** 遍历 sources,把 Mesh / LineSegments 克隆到世界空间并收集(跳过 name === "capsule")。 */
function collectWorldSpaceGeometries(sources: THREE.Object3D[]): THREE.BufferGeometry[] {
const collected: THREE.BufferGeometry[] = [];
const collectMesh = (mesh: THREE.Mesh | THREE.LineSegments) => {
try {
let geom = mesh.geometry.clone();
geom.applyMatrix4(mesh.matrixWorld);
if (geom.index) geom = geom.toNonIndexed();
const safe = ensureAttributesMinimal(geom);
if (safe) collected.push(safe);
} catch (e) {
console.warn("处理网格时出错:", mesh, e);
}
};
for (const obj of sources) {
obj.updateMatrixWorld(true);
obj.traverse(c => {
const a = c as any;
if (
(a.isMesh || a.isLineSegments)
&& a.geometry
&& c.name !== "capsule"
&& !isExcludedFromCollider(c)
) collectMesh(a);
});
}
return collected;
}
const _rtcTranslation = new THREE.Matrix4();
const _rtcCenter = new THREE.Vector3();
/**
* 把合并后的几何收到包围盒中心附近,避免大坐标留在 Float32 顶点里。
* 返回平移到原点的中心,调用方需把它编进 bakeInverse。
*/
function recenterGeometry(geometry: THREE.BufferGeometry, target: THREE.Vector3): THREE.Vector3 {
geometry.computeBoundingBox();
const box = geometry.boundingBox;
if (!box || box.isEmpty()) {
target.set(0, 0, 0);
return target;
}
box.getCenter(target);
if (target.lengthSq() === 0) return target;
geometry.translate(-target.x, -target.y, -target.z);
geometry.computeBoundingBox();
geometry.computeBoundingSphere();
return target;
}
/**
* 用合并几何创建线框调试 Mesh,并挂上 acceleratedRaycast。
* localSpace 时关闭 matrixAutoUpdate,由外部每帧对齐 follow。
*/
function makeDebugMesh(geometry: THREE.BufferGeometry, localSpace: boolean): THREE.Mesh {
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({
opacity: 0.5,
transparent: true,
wireframe: true,
depthTest: true,
side: THREE.DoubleSide,
}),
);
mesh.raycast = acceleratedRaycast;
if (localSpace) {
mesh.matrixAutoUpdate = false;
} else {
mesh.layers.enable(1);
}
return mesh;
}
/** build*MergedMesh 的返回值:调试用 mesh + 待挂 BVH 的 geometry。 */
export type BuiltMeshCollider = {
mesh: THREE.Mesh;
geometry: THREE.BufferGeometry;
/** 运动学烘焙:inv(source.matrixWorld) * T(rtcCenter)。*/
bakeInverse?: THREE.Matrix4;
};
/**
* 合并 source 为世界空间静态碰撞网格。
* 仅合并几何,不挂 BVH;调用方需再走 attachBoundsTree,否则不能参与碰撞查询。
*/
export function buildStaticMergedMesh(sources: THREE.Object3D | THREE.Object3D[]): BuiltMeshCollider | null {
const list = Array.isArray(sources) ? sources : [sources];
let collected = collectWorldSpaceGeometries(list);
if (!collected.length) return null;
collected = unifiedAttribute(collected);
const merged = BufferGeometryUtils.mergeGeometries(collected, false);
if (!merged) {
console.error("合并几何失败");
return null;
}
return { mesh: makeDebugMesh(merged, false), geometry: merged };
}
/**
* 合并 source 为运动学碰撞网格。
* 先乘 matrixWorld 落到当前世界 / 局部 ENU,再绕包围盒中心收成小坐标;
* 调用方需再走 attachBoundsTree,并每帧用
* source.matrixWorld * bakeInverse 跟随。
*/
export function buildKinematicMergedMesh(source: THREE.Object3D): BuiltMeshCollider | null {
source.updateMatrixWorld(true);
let collected = collectWorldSpaceGeometries([source]);
if (!collected.length) return null;
collected = unifiedAttribute(collected);
const merged = BufferGeometryUtils.mergeGeometries(collected, false);
if (!merged) {
console.error("合并运动学几何失败");
return null;
}
const rtcCenter = recenterGeometry(merged, _rtcCenter);
const bakeInverse = new THREE.Matrix4().copy(source.matrixWorld).invert();
bakeInverse.multiply(_rtcTranslation.makeTranslation(rtcCenter.x, rtcCenter.y, rtcCenter.z));
const mesh = makeDebugMesh(merged, true);
mesh.matrix.copy(source.matrixWorld).multiply(bakeInverse);
mesh.updateMatrixWorld(true);
return { mesh, geometry: merged, bakeInverse };
}
/**
* 为几何挂上 MeshBVH(写入 geometry.boundsTree)。
* useWorker + workerPool 时异步构建,失败回退主线程;onStale 为真则丢弃过期结果。
*/
export function attachBoundsTree(
geometry: THREE.BufferGeometry,
options: {
useWorker?: boolean;
workerPool?: BvhWorkerPool;
maxDepth?: number;
onReady: (bvh: MeshBVH) => void;
onStale?: () => boolean;
},
): void {
const maxDepth = options.maxDepth;
const sync = () => {
const bvh = maxDepth != null
? new MeshBVH(geometry, { maxDepth })
: new MeshBVH(geometry);
(geometry as any).boundsTree = bvh;
options.onReady(bvh);
};
if (!options.useWorker || !options.workerPool) {
sync();
return;
}
void options.workerPool.generate(geometry).then((bvh) => {
if (options.onStale?.()) return;
(geometry as any).boundsTree = bvh;
options.onReady(bvh);
}).catch((error) => {
if (options.onStale?.()) return;
console.warn("异步构建 BVH 失败,回退到主线程:", error);
sync();
});
}
@@ -0,0 +1,111 @@
import type * as THREE from "three";
import type { MotionType } from "./CollisionWorld";
/** 从场景节点采集并合并的三角网格。 */
export type MeshSourceShape = {
kind: "mesh";
/** 采集几何的来源;必传。 */
source: THREE.Object3D | THREE.Object3D[];
mesh?: never;
};
/** 已有(可含 BVH)的碰撞网格。 */
export type MeshReadyShape = {
kind: "mesh";
mesh: THREE.Mesh;
source?: never;
};
export type MeshShape = MeshSourceShape | MeshReadyShape;
export type BoxShape = {
kind: "box";
halfExtents: THREE.Vector3;
/** 世界位置,或相对 follow 的局部偏移。 */
position?: THREE.Vector3;
quaternion?: THREE.Quaternion;
};
export type SphereShape = {
kind: "sphere";
radius: number;
/** 世界位置,或相对 follow 的局部偏移。 */
position?: THREE.Vector3;
};
export type ColliderShape = MeshShape | BoxShape | SphereShape;
type ColliderDescBase = {
groups?: number;
mask?: number;
useWorker?: boolean;
userData?: unknown;
};
export type StaticColliderDesc = ColliderDescBase & {
motion: "static";
shape: ColliderShape;
follow?: never;
};
export type KinematicColliderDesc = ColliderDescBase & {
motion: "kinematic";
shape: ColliderShape;
/** 每帧碰撞体对齐该节点的 matrixWorld;mesh+source 时默认跟 source。 */
follow?: THREE.Object3D;
};
type DynamicColliderMaterialFields = {
/** 密度,质量 = 体积 × 密度;默认见 DYNAMIC_BODY_DEFAULTS。 */
density?: number;
/** 弹性,0 不反弹,1 完全弹;默认见 DYNAMIC_BODY_DEFAULTS。 */
restitution?: number;
/** 摩擦;默认见 DYNAMIC_BODY_DEFAULTS。 */
friction?: number;
/** 线阻尼,越大滑行越快停;默认见 DYNAMIC_BODY_DEFAULTS。 */
linearDamping?: number;
/** 角阻尼,越大旋转越快停;默认见 DYNAMIC_BODY_DEFAULTS。 */
angularDamping?: number;
gravity?: number;
velocity?: THREE.Vector3;
angularVelocity?: THREE.Vector3;
};
/** 动态碰撞体描述。 */
export type DynamicColliderDesc = ColliderDescBase & DynamicColliderMaterialFields & {
motion: "dynamic";
shape: ColliderShape;
follow?: never;
/** 可选视觉网格。 */
mesh?: THREE.Mesh;
/**
* 是否由 DynamicBodySystem 创建并推进刚体。
* - 默认 true(省略时):球 / 可推箱子等会创建 DynamicBody
* - false:只登记 CollisionWorld,不创建 DynamicBody(车辆底盘由 VehicleRigidBody 推进)
*/
simulate?: boolean;
};
/** 动态刚体材质默认值。 */
export const DYNAMIC_BODY_DEFAULTS = {
density: 1,
restitution: 0.2,
friction: 0.6,
linearDamping: 0.4,
angularDamping: 0.6,
};
/** 碰撞体创建描述。 */
export type ColliderDesc = StaticColliderDesc | KinematicColliderDesc | DynamicColliderDesc;
/** addCollider 返回的句柄。 */
export type ColliderHandle = {
id: number;
motion: MotionType;
/** 运动学跟随目标(若有)。 */
follow: THREE.Object3D | null;
/** mesh 采集源(若有)。 */
source: THREE.Object3D | null;
/** 用于 BVH 查询的网格(static/kinematic mesh)。 */
collisionMesh: THREE.Mesh | null;
};
@@ -0,0 +1,85 @@
import * as THREE from "three";
import { ContactManifold } from "./ContactManifold";
import { ContactPoint } from "./ContactPoint";
import { normalClass } from "./contactReducer";
const MATCH_NORMAL = 0.95; // 法线点积低于此值视为不同流形
const MATCH_DISTANCE = 0.08; // 局部接触点匹配距离上限
/** findMatch 的命中结果。 */
type MatchResult = {
point: ContactPoint; // 上一帧匹配到的接触点
normal: THREE.Vector3; // 上一帧流形法线
};
/**
* 帧间接触点匹配,用于冲量热启动(warm start)。
* 求解前 match 继承冲量;求解后 save 供下一帧使用。
*/
export class ContactCache {
private prev: ContactManifold[] = []; // 上一帧流形快照
/**
* 将上一帧冲量拷到本帧对应接触点。
* 无匹配、或支撑面 ↔ 墙面切换时清零,避免错误热启动。
*/
match(manifolds: ContactManifold[]): void {
for (const manifold of manifolds) {
for (const point of manifold.contacts) {
const prev = this.findMatch(manifold.normal, point);
if (!prev) {
point.resetImpulses();
continue;
}
// 法线类别突变(地面 ↔ 墙)时冲量不可复用
const prevClass = normalClass(prev.normal);
const nextClass = normalClass(manifold.normal);
if (
(prevClass === "support" && nextClass === "wall") ||
(prevClass === "wall" && nextClass === "support")
) {
point.resetImpulses();
continue;
}
point.normalImpulse = prev.point.normalImpulse;
point.tangentImpulse1 = prev.point.tangentImpulse1;
point.tangentImpulse2 = prev.point.tangentImpulse2;
}
}
}
/** 深拷贝本帧流形与接触冲量,供下一帧 match。 */
save(manifolds: ContactManifold[]): void {
this.prev = manifolds.map((src) => {
const copy = new ContactManifold();
copy.normal.copy(src.normal);
copy.normalSum.copy(src.normalSum);
copy.contacts = src.contacts.map((p) => {
const c = new ContactPoint();
c.copyFrom(p);
return c;
});
return copy;
});
}
/**
* 在上一帧中找法线接近且局部点最近的接触。
* 用刚体局部坐标匹配,避免物体移动后面世界点对不上。
*/
private findMatch(normal: THREE.Vector3, point: ContactPoint): MatchResult | null {
let best: MatchResult | null = null;
let bestDist = MATCH_DISTANCE * MATCH_DISTANCE;
for (const manifold of this.prev) {
if (manifold.normal.dot(normal) < MATCH_NORMAL) continue;
for (const prev of manifold.contacts) {
const d = prev.localPoint.distanceToSquared(point.localPoint);
if (d < bestDist) {
bestDist = d;
best = { point: prev, normal: manifold.normal };
}
}
}
return best;
}
}
@@ -0,0 +1,15 @@
import * as THREE from "three";
import { ContactPoint } from "./ContactPoint";
/** 同一法线簇下的接触点集合。 */
export class ContactManifold {
normal = new THREE.Vector3();
normalSum = new THREE.Vector3();
contacts: ContactPoint[] = [];
clear(): void {
this.contacts.length = 0;
this.normal.set(0, 1, 0);
this.normalSum.set(0, 0, 0);
}
}
@@ -0,0 +1,48 @@
import * as THREE from "three";
/** 标定用特征半尺寸;skin/slop 按 extent 相对此值缩放。 */
export const CONTACT_REF_EXTENT = 0.2;
export const CONTACT_SKIN = 0.002;
/** 按刚体特征尺寸缩放接触皮肤,避免小物体相对厚度过大。 */
export function contactSkinForExtent(extent: number): number {
const s = Math.max(1e-4, extent) / CONTACT_REF_EXTENT;
return CONTACT_SKIN * s;
}
export type RawContact = {
point: THREE.Vector3;
normal: THREE.Vector3;
penetration: number;
};
/** 约束求解用的单个接触点。 */
export class ContactPoint {
worldPoint = new THREE.Vector3();
localPoint = new THREE.Vector3();
penetration = 0;
biasVelocity = 0;
normalImpulse = 0;
tangentImpulse1 = 0;
tangentImpulse2 = 0;
tangent1 = new THREE.Vector3();
tangent2 = new THREE.Vector3();
copyFrom(other: ContactPoint): void {
this.worldPoint.copy(other.worldPoint);
this.localPoint.copy(other.localPoint);
this.penetration = other.penetration;
this.normalImpulse = other.normalImpulse;
this.tangentImpulse1 = other.tangentImpulse1;
this.tangentImpulse2 = other.tangentImpulse2;
this.tangent1.copy(other.tangent1);
this.tangent2.copy(other.tangent2);
}
resetImpulses(): void {
this.normalImpulse = 0;
this.tangentImpulse1 = 0;
this.tangentImpulse2 = 0;
}
}
@@ -0,0 +1,201 @@
import * as THREE from "three";
import type { ImpulseBody } from "../solver/ImpulseBody";
import { ContactManifold } from "./ContactManifold";
import { ContactPoint, CONTACT_REF_EXTENT, contactSkinForExtent, type RawContact } from "./ContactPoint";
const NORMAL_MERGE = 0.98; // 法线点积高于此值并入同一流形
const MAX_POINTS = 4; // 单流形最多保留的接触点
const SUPPORT_Y = 0.7; // ny 以上视为支撑面(地面)
const WALL_Y = 0.35; // ny 以下视为墙面
const _invQuat = new THREE.Quaternion();
const _ab = new THREE.Vector3();
const _ac = new THREE.Vector3();
const _ap = new THREE.Vector3();
const _n = new THREE.Vector3();
/**
* 将原始接触按法线聚类,并裁成至多 4 个支撑点。
* 返回写入后的 out(长度收束为有效流形数)。
*/
export function reduceContacts(
raw: RawContact[],
count: number,
body: ImpulseBody,
out: ContactManifold[],
): ContactManifold[] {
for (const m of out) m.clear();
let manifoldCount = 0;
// 按法线合并进流形,并累计 normalSum
const skin = contactSkinForExtent(body.characteristicExtent?.() ?? CONTACT_REF_EXTENT);
for (let r = 0; r < count; r++) {
const contact = raw[r];
if (contact.penetration < -skin || contact.normal.lengthSq() < 1e-8) continue;
_n.copy(contact.normal).normalize();
let cluster = -1;
for (let i = 0; i < manifoldCount; i++) {
if (out[i].normal.dot(_n) > NORMAL_MERGE) {
cluster = i;
break;
}
}
if (cluster < 0) {
if (manifoldCount === out.length) {
out.push(new ContactManifold());
}
cluster = manifoldCount;
manifoldCount += 1;
out[cluster].normalSum.copy(_n);
out[cluster].normal.copy(_n);
} else {
out[cluster].normalSum.add(_n);
out[cluster].normal.copy(out[cluster].normalSum).normalize();
}
out[cluster].contacts.push(makePoint(contact, body));
}
// 定稿法线、裁点、建切向基
for (let i = 0; i < manifoldCount; i++) {
const manifold = out[i];
if (manifold.normalSum.lengthSq() < 1e-8) manifold.normal.set(0, 1, 0);
else manifold.normal.copy(manifold.normalSum).normalize();
if (manifold.contacts.length > MAX_POINTS) {
manifold.contacts = pickSupportPoints(manifold.contacts);
}
for (const point of manifold.contacts) {
buildTangents(manifold.normal, point);
}
}
out.length = manifoldCount;
stabilizeEdgeManifolds(out);
return out;
}
/** 按法线竖直分量分类:支撑面 / 墙 / 斜面。 */
export function normalClass(normal: THREE.Vector3): "support" | "wall" | "slant" {
if (normal.y >= SUPPORT_Y) return "support";
if (normal.y <= WALL_Y) return "wall";
return "slant";
}
/**
* 边角同时碰到地面与墙时丢掉斜面流形,避免错误推力。
* 若没有支撑面,则把斜面法线抬到至少 SUPPORT_Y,减轻卡边抖动。
*/
function stabilizeEdgeManifolds(manifolds: ContactManifold[]): void {
let hasSupport = false;
let hasWall = false;
for (const m of manifolds) {
const cls = normalClass(m.normal);
if (cls === "support") hasSupport = true;
else if (cls === "wall") hasWall = true;
}
if (hasSupport && hasWall) {
let write = 0;
for (let i = 0; i < manifolds.length; i++) {
if (normalClass(manifolds[i].normal) === "slant") continue;
if (write !== i) manifolds[write] = manifolds[i];
write += 1;
}
manifolds.length = write;
return;
}
if (!hasSupport) {
for (const m of manifolds) {
if (normalClass(m.normal) !== "slant") continue;
m.normal.y = Math.max(m.normal.y, SUPPORT_Y);
m.normal.normalize();
m.normalSum.copy(m.normal);
for (const point of m.contacts) {
buildTangents(m.normal, point);
}
}
}
}
/** 由原始接触生成 ContactPoint,并写入刚体局部坐标(供帧间匹配)。 */
function makePoint(raw: RawContact, body: ImpulseBody): ContactPoint {
const point = new ContactPoint();
point.worldPoint.copy(raw.point);
point.penetration = raw.penetration;
_invQuat.copy(body.quaternion).invert();
point.localPoint.subVectors(raw.point, body.position).applyQuaternion(_invQuat);
return point;
}
/**
* 从过多接触点中挑最多 4 个:最深穿透 → 最远点 → 最大面积第三点 → 离平面最远第四点。
* 用于稳定盒/车身 resting 接触。
*/
function pickSupportPoints(points: ContactPoint[]): ContactPoint[] {
if (points.length <= MAX_POINTS) return points;
// p0:穿透最深
let i0 = 0;
for (let i = 1; i < points.length; i++) {
if (points[i].penetration > points[i0].penetration) i0 = i;
}
const p0 = points[i0];
// p1:离 p0 最远
let i1 = i0;
let best = -1;
for (let i = 0; i < points.length; i++) {
const d = points[i].worldPoint.distanceToSquared(p0.worldPoint);
if (d > best) {
best = d;
i1 = i;
}
}
const p1 = points[i1];
// p2:与 p0p1 构成面积最大
let i2 = i0;
best = -1;
_ab.subVectors(p1.worldPoint, p0.worldPoint);
for (let i = 0; i < points.length; i++) {
_ap.subVectors(points[i].worldPoint, p0.worldPoint);
const area = _ap.cross(_ab).lengthSq();
if (area > best) {
best = area;
i2 = i;
}
}
const p2 = points[i2];
// p3:离 p0p1p2 平面最远(略加权穿透)
let i3 = i0;
best = -1;
_ab.subVectors(p1.worldPoint, p0.worldPoint);
_ac.subVectors(p2.worldPoint, p0.worldPoint);
_n.copy(_ab).cross(_ac);
for (let i = 0; i < points.length; i++) {
if (i === i0 || i === i1 || i === i2) continue;
_ap.subVectors(points[i].worldPoint, p0.worldPoint);
const d = Math.abs(_ap.dot(_n));
const score = d + points[i].penetration * 0.01;
if (score > best) {
best = score;
i3 = i;
}
}
const picked = [p0];
if (i1 !== i0) picked.push(p1);
if (i2 !== i0 && i2 !== i1) picked.push(p2);
if (i3 !== i0 && i3 !== i1 && i3 !== i2 && picked.length < MAX_POINTS) picked.push(points[i3]);
return picked;
}
/** 由流形法线构造正交切向基(摩擦两轴)。 */
function buildTangents(normal: THREE.Vector3, point: ContactPoint): void {
if (Math.abs(normal.y) < 0.99) point.tangent1.set(0, 1, 0).cross(normal);
else point.tangent1.set(1, 0, 0).cross(normal); // 法线近竖直时换参考轴
if (point.tangent1.lengthSq() < 1e-8) point.tangent1.set(0, 0, 1).cross(normal);
point.tangent1.normalize();
point.tangent2.copy(normal).cross(point.tangent1).normalize();
}
@@ -0,0 +1,20 @@
/** 碰撞业务分组 bitmask(与 MotionType 正交)。 */
export const CollisionGroup = {
ALL: 0xffff,
/** 场景静态 / 运动学网格等默认层。 */
DEFAULT: 1 << 0,
/** 人物相关(预留)。 */
CHARACTER: 1 << 1,
/** 车辆底盘盒。 */
VEHICLE: 1 << 2,
/** 动态碎片(球等)。 */
DEBRIS: 1 << 3,
} as const;
/** 人物胶囊 / 相机射线的单向查询目标。 */
export const CHARACTER_QUERY_MASK = CollisionGroup.DEFAULT;
/** 全部分组掩码。 */
export const ALL_COLLISION_MASK = CollisionGroup.ALL;
export type CollisionGroupBits = (typeof CollisionGroup)[keyof typeof CollisionGroup];
@@ -0,0 +1,31 @@
export type { MotionType, CollisionCollider, CollisionShape, AddColliderOptions, MeshQueryFilter, MeshQueryOptions } from "./CollisionWorld";
export { CollisionWorld } from "./CollisionWorld";
export {
CollisionGroup,
CHARACTER_QUERY_MASK,
ALL_COLLISION_MASK,
type CollisionGroupBits,
} from "./groups";
export type {
ColliderDesc,
ColliderHandle,
ColliderShape,
MeshShape,
BoxShape,
SphereShape,
StaticColliderDesc,
KinematicColliderDesc,
DynamicColliderDesc,
} from "./colliderDesc";
export { DYNAMIC_BODY_DEFAULTS } from "./colliderDesc";
export { DynamicBody } from "./DynamicBody";
export type { DynamicBodyKind, DynamicBodyMaterialOptions } from "./DynamicBody";
export { DynamicSphereBody, isSphereBody } from "./DynamicSphere";
export { DynamicBoxBody, isBoxBody } from "./DynamicBox";
export {
buildStaticMergedMesh,
buildKinematicMergedMesh,
attachBoundsTree,
ensureAttributesMinimal,
unifiedAttribute,
} from "./colliderBuild";
@@ -0,0 +1,223 @@
import * as THREE from "three";
import { impulseDenominator } from "../utils/vehiclePhysics/vehicleMath";
import type { ImpulseBody } from "./solver/ImpulseBody";
const EPS = 1e-8; // 退化轴 / 零长度下限
const AXIS_EPS = 1e-10; // 平行边叉积视为退化
const BAUMGARTE = 0.8; // 每帧推出重叠的比例
const RESTITUTION = 0.2; // 车车法向弹性
const FRICTION = 0.4; // 切向摩擦相对法向冲量的上限
/** OBB–OBB 解算所需的刚体字段(扩展共享 ImpulseBody)。 */
export type ObbRigidBody = ImpulseBody & {
halfExtents: THREE.Vector3; // 碰撞盒半边长
};
/** OBB SAT / 接触冲量的预分配临时量。 */
export type ObbObbTemps = {
ax: THREE.Vector3; // A 盒世界轴 X
ay: THREE.Vector3;
az: THREE.Vector3;
bx: THREE.Vector3; // B 盒世界轴 X
by: THREE.Vector3;
bz: THREE.Vector3;
axis: THREE.Vector3; // 当前测试分离轴
bestN: THREE.Vector3; // 最小平移法线(A → B)
tangent: THREE.Vector3; // 摩擦切向
invQuatA: THREE.Quaternion;
invQuatB: THREE.Quaternion;
local: THREE.Vector3; // 支撑点局部缓冲
supportA: THREE.Vector3; // A 沿法线的支撑点
supportB: THREE.Vector3; // B 沿反法线的支撑点
contact: THREE.Vector3; // 世界接触点(两支撑中点)
velA: THREE.Vector3; // 接触点处 A 速度
velB: THREE.Vector3;
rel: THREE.Vector3; // 质心差 / 相对速度缓冲
impulse: THREE.Vector3;
r: THREE.Vector3; // impulseDenominator 用
gcross: THREE.Vector3;
localI: THREE.Vector3;
worldI: THREE.Vector3;
invQuat: THREE.Quaternion;
};
/** 创建 OBB 碰撞临时量。 */
export function createObbObbTemps(): ObbObbTemps {
return {
ax: new THREE.Vector3(),
ay: new THREE.Vector3(),
az: new THREE.Vector3(),
bx: new THREE.Vector3(),
by: new THREE.Vector3(),
bz: new THREE.Vector3(),
axis: new THREE.Vector3(),
bestN: new THREE.Vector3(),
tangent: new THREE.Vector3(),
invQuatA: new THREE.Quaternion(),
invQuatB: new THREE.Quaternion(),
local: new THREE.Vector3(),
supportA: new THREE.Vector3(),
supportB: new THREE.Vector3(),
contact: new THREE.Vector3(),
velA: new THREE.Vector3(),
velB: new THREE.Vector3(),
rel: new THREE.Vector3(),
impulse: new THREE.Vector3(),
r: new THREE.Vector3(),
gcross: new THREE.Vector3(),
localI: new THREE.Vector3(),
worldI: new THREE.Vector3(),
invQuat: new THREE.Quaternion(),
};
}
/** 盒在轴 n 上的投影半径(半宽之和)。 */
function radiusOnAxis(ax: THREE.Vector3, ay: THREE.Vector3, az: THREE.Vector3, half: THREE.Vector3, n: THREE.Vector3): number {
return half.x * Math.abs(n.dot(ax)) + half.y * Math.abs(n.dot(ay)) + half.z * Math.abs(n.dot(az));
}
/**
* 盒在 dir 方向上的支撑点(取各轴符号 * 半边,面法向时落在对应面心附近)。
*/
function supportPoint(
pos: THREE.Vector3,
quat: THREE.Quaternion,
invQuat: THREE.Quaternion,
half: THREE.Vector3,
dir: THREE.Vector3,
local: THREE.Vector3,
out: THREE.Vector3,
): THREE.Vector3 {
local.copy(dir).applyQuaternion(invQuat);
out.set(
Math.sign(local.x) * half.x,
Math.sign(local.y) * half.y,
Math.sign(local.z) * half.z,
);
return out.applyQuaternion(quat).add(pos);
}
/**
* 两车底盘 OBB:SAT 最小平移推出,再按质量在接触点施法向 / 摩擦冲量。
* `temps.bestN` 指向从 A 到 B;有重叠并处理则返回 true。
*/
export function resolveObbObb(a: ObbRigidBody, b: ObbRigidBody, temps: ObbObbTemps): boolean {
// 两盒局部轴变到世界
temps.ax.set(1, 0, 0).applyQuaternion(a.quaternion);
temps.ay.set(0, 1, 0).applyQuaternion(a.quaternion);
temps.az.set(0, 0, 1).applyQuaternion(a.quaternion);
temps.bx.set(1, 0, 0).applyQuaternion(b.quaternion);
temps.by.set(0, 1, 0).applyQuaternion(b.quaternion);
temps.bz.set(0, 0, 1).applyQuaternion(b.quaternion);
let minOverlap = Infinity;
/** 测试一根分离轴:无重叠返回 false;否则更新最小平移轴。 */
const tryAxis = (x: number, y: number, z: number): boolean => {
temps.axis.set(x, y, z);
const lenSq = temps.axis.lengthSq();
if (lenSq < AXIS_EPS) return true; // 平行边叉积退化,跳过
temps.axis.multiplyScalar(1 / Math.sqrt(lenSq));
// 重叠深度 = 两投影半径之和 - 质心差在轴上的投影
const overlap =
radiusOnAxis(temps.ax, temps.ay, temps.az, a.halfExtents, temps.axis)
+ radiusOnAxis(temps.bx, temps.by, temps.bz, b.halfExtents, temps.axis)
- Math.abs(temps.axis.dot(temps.rel.subVectors(b.position, a.position)));
if (overlap <= EPS) return false; // 分离
if (overlap < minOverlap) {
minOverlap = overlap;
temps.bestN.copy(temps.axis);
// 法线统一为 A → B
if (temps.bestN.dot(temps.rel) < 0) temps.bestN.negate();
}
return true;
};
// 面法线:A、B 各 3 轴
if (!tryAxis(temps.ax.x, temps.ax.y, temps.ax.z)) return false;
if (!tryAxis(temps.ay.x, temps.ay.y, temps.ay.z)) return false;
if (!tryAxis(temps.az.x, temps.az.y, temps.az.z)) return false;
if (!tryAxis(temps.bx.x, temps.bx.y, temps.bx.z)) return false;
if (!tryAxis(temps.by.x, temps.by.y, temps.by.z)) return false;
if (!tryAxis(temps.bz.x, temps.bz.y, temps.bz.z)) return false;
// 边叉积:A 的 3 边 × B 的 3 边
temps.axis.copy(temps.ax).cross(temps.bx);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.ax).cross(temps.by);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.ax).cross(temps.bz);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.ay).cross(temps.bx);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.ay).cross(temps.by);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.ay).cross(temps.bz);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.az).cross(temps.bx);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.az).cross(temps.by);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
temps.axis.copy(temps.az).cross(temps.bz);
if (!tryAxis(temps.axis.x, temps.axis.y, temps.axis.z)) return false;
if (!Number.isFinite(minOverlap)) return false;
// 位置修正:深度钳制后按 invMass 分配,乘 Baumgarte
const minHalf = Math.min(
a.halfExtents.x, a.halfExtents.y, a.halfExtents.z,
b.halfExtents.x, b.halfExtents.y, b.halfExtents.z,
);
const depth = Math.min(minOverlap, minHalf * 2) * BAUMGARTE;
const invSum = a.invMass + b.invMass;
if (invSum <= EPS || depth <= EPS) return false;
// 接触点 ≈ 两盒沿法线方向支撑点的中点
temps.invQuatA.copy(a.quaternion).invert();
temps.invQuatB.copy(b.quaternion).invert();
supportPoint(a.position, a.quaternion, temps.invQuatA, a.halfExtents, temps.bestN, temps.local, temps.supportA);
supportPoint(b.position, b.quaternion, temps.invQuatB, b.halfExtents, temps.rel.copy(temps.bestN).negate(), temps.local, temps.supportB);
temps.contact.copy(temps.supportA).add(temps.supportB).multiplyScalar(0.5);
a.position.addScaledVector(temps.bestN, -depth * (a.invMass / invSum));
b.position.addScaledVector(temps.bestN, depth * (b.invMass / invSum));
// 法向冲量:仅处理接近中的相对速度
a.getVelocityAtPoint(temps.contact, temps.velA);
b.getVelocityAtPoint(temps.contact, temps.velB);
const relN = temps.velB.dot(temps.bestN) - temps.velA.dot(temps.bestN);
if (relN >= 0) return true;
const denomN =
impulseDenominator(a, temps.contact, temps.bestN, temps.r, temps.gcross, temps.localI, temps.worldI, temps.invQuat)
+ impulseDenominator(b, temps.contact, temps.bestN, temps.r, temps.gcross, temps.localI, temps.worldI, temps.invQuat);
if (denomN <= EPS) return true;
const jn = -(1 + RESTITUTION) * relN / denomN;
temps.impulse.copy(temps.bestN).multiplyScalar(-jn);
a.applyImpulseAtPoint(temps.impulse, temps.contact);
temps.impulse.copy(temps.bestN).multiplyScalar(jn);
b.applyImpulseAtPoint(temps.impulse, temps.contact);
// 库仑摩擦:切向相对速度,冲量钳在 μ|jn|
a.getVelocityAtPoint(temps.contact, temps.velA);
b.getVelocityAtPoint(temps.contact, temps.velB);
temps.rel.subVectors(temps.velA, temps.velB);
temps.rel.addScaledVector(temps.bestN, -temps.rel.dot(temps.bestN)); // 去掉法向分量
const tLen = temps.rel.length();
if (tLen <= EPS) return true;
temps.tangent.copy(temps.rel).multiplyScalar(1 / tLen);
const denomT =
impulseDenominator(a, temps.contact, temps.tangent, temps.r, temps.gcross, temps.localI, temps.worldI, temps.invQuat)
+ impulseDenominator(b, temps.contact, temps.tangent, temps.r, temps.gcross, temps.localI, temps.worldI, temps.invQuat);
if (denomT <= EPS) return true;
let jt = -tLen / denomT;
const maxF = FRICTION * Math.abs(jn);
jt = Math.max(-maxF, Math.min(maxF, jt));
temps.impulse.copy(temps.tangent).multiplyScalar(jt);
a.applyImpulseAtPoint(temps.impulse, temps.contact);
temps.impulse.negate();
b.applyImpulseAtPoint(temps.impulse, temps.contact);
return true;
}
@@ -0,0 +1,56 @@
import type { ContactManifold } from "../contacts/ContactManifold";
import type { ImpulseBody } from "./ImpulseBody";
import { solveFrictionConstraint, warmStartFriction } from "./frictionConstraint";
import {
prepareNormalConstraint,
solveNormalConstraint,
warmStartNormal,
} from "./normalConstraint";
/**
* 共享接触冲量求解器:法向 + 摩擦锥,可选热启动。
* 人物胶囊不走此核(位置推出);轮悬架 / 轮胎仍在车辆模块。
*/
export class ContactImpulseSolver {
velocityIterations = 8; // 速度迭代次数
warmStart = true; // 是否复用上一帧冲量做热启动
/**
* 对单个刚体与一组流形做速度级求解。
* 顺序:准备 bias → 热启动 → 交替解法向 / 摩擦。
*/
solveVelocity(
body: ImpulseBody,
manifolds: ContactManifold[],
dt: number,
): void {
if (!manifolds.length) return;
// 写入弹性与穿透修正用的 biasVelocity
for (const manifold of manifolds) {
for (const point of manifold.contacts) {
prepareNormalConstraint(body, manifold, point, dt);
}
}
// 先施加缓存冲量,减少 resting 抖动
if (this.warmStart) {
for (const manifold of manifolds) {
for (const point of manifold.contacts) {
warmStartNormal(body, manifold, point);
warmStartFriction(body, point);
}
}
}
// PGS:每轮先法向后摩擦
for (let i = 0; i < this.velocityIterations; i++) {
for (const manifold of manifolds) {
for (const point of manifold.contacts) {
solveNormalConstraint(body, manifold, point);
solveFrictionConstraint(body, manifold, point);
}
}
}
}
}
@@ -0,0 +1,28 @@
import type * as THREE from "three";
/**
* 接触冲量求解所需的刚体接口。
*/
export type ImpulseBody = {
/** 世界质心位置。 */
position: THREE.Vector3;
/** 世界朝向。 */
quaternion: THREE.Quaternion;
/** 质量倒数。 */
invMass: number;
/** 局部惯量倒数对角元。 */
invInertia: THREE.Vector3;
/** 摩擦系数 μ;缺省由摩擦约束使用默认值。 */
friction?: number;
/** 弹性,0 不反弹,1 完全弹;缺省 0。 */
restitution?: number;
/**
* 特征半尺寸(用于缩放接触 skin / 穿透 slop)。
* 缺省按 CONTACT_REF_EXTENT,不缩放。
*/
characteristicExtent?: () => number;
/** 读取世界点处速度:v + ω × r。 */
getVelocityAtPoint(point: THREE.Vector3, out: THREE.Vector3): THREE.Vector3;
/** 在世界点施加冲量(线速度 + 角速度)。 */
applyImpulseAtPoint(impulse: THREE.Vector3, point: THREE.Vector3): void;
};
@@ -0,0 +1,73 @@
import * as THREE from "three";
import { effectiveMass } from "../../utils/vehiclePhysics/vehicleMath";
import type { ContactManifold } from "../contacts/ContactManifold";
import type { ContactPoint } from "../contacts/ContactPoint";
import type { ImpulseBody } from "./ImpulseBody";
const DEFAULT_FRICTION = 0.6; // body.friction 未设时的库仑系数 μ
const _r = new THREE.Vector3();
const _gcross = new THREE.Vector3();
const _local = new THREE.Vector3();
const _world = new THREE.Vector3();
const _invQuat = new THREE.Quaternion();
const _vel = new THREE.Vector3();
const _impulse = new THREE.Vector3();
/** 热启动:施加上一帧累计的切向冲量。 */
export function warmStartFriction(
body: ImpulseBody,
point: ContactPoint,
): void {
if (point.tangentImpulse1 === 0 && point.tangentImpulse2 === 0) return;
_impulse.copy(point.tangent1).multiplyScalar(point.tangentImpulse1);
_impulse.addScaledVector(point.tangent2, point.tangentImpulse2);
body.applyImpulseAtPoint(_impulse, point.worldPoint);
}
/**
* 解切向摩擦约束(库仑锥)。
* 上限 maxF = μ · 法向冲量;超出则等比缩放两切向冲量。
*/
export function solveFrictionConstraint(
body: ImpulseBody,
_manifold: ContactManifold,
point: ContactPoint,
): void {
const mu = body.friction ?? DEFAULT_FRICTION;
const maxF = mu * point.normalImpulse;
if (maxF <= 1e-8) {
// 无法向压力则无摩擦
point.tangentImpulse1 = 0;
point.tangentImpulse2 = 0;
return;
}
body.getVelocityAtPoint(point.worldPoint, _vel);
const vt1 = _vel.dot(point.tangent1);
const vt2 = _vel.dot(point.tangent2);
const mass1 = effectiveMass(body, point.worldPoint, point.tangent1, _r, _gcross, _local, _world, _invQuat);
const mass2 = effectiveMass(body, point.worldPoint, point.tangent2, _r, _gcross, _local, _world, _invQuat);
// 累计切向冲量,使切向相对速度趋近 0
const old1 = point.tangentImpulse1;
const old2 = point.tangentImpulse2;
point.tangentImpulse1 += mass1 > 0 ? -vt1 * mass1 : 0;
point.tangentImpulse2 += mass2 > 0 ? -vt2 * mass2 : 0;
// 钳到摩擦锥:‖λt‖ ≤ μ λn
const magSq = point.tangentImpulse1 * point.tangentImpulse1 + point.tangentImpulse2 * point.tangentImpulse2;
const maxSq = maxF * maxF;
if (magSq > maxSq && magSq > 1e-12) {
const scale = maxF / Math.sqrt(magSq);
point.tangentImpulse1 *= scale;
point.tangentImpulse2 *= scale;
}
// 只施加本轮增量
const d1 = point.tangentImpulse1 - old1;
const d2 = point.tangentImpulse2 - old2;
if (d1 === 0 && d2 === 0) return;
_impulse.copy(point.tangent1).multiplyScalar(d1).addScaledVector(point.tangent2, d2);
body.applyImpulseAtPoint(_impulse, point.worldPoint);
}
@@ -0,0 +1,92 @@
import * as THREE from "three";
import { effectiveMass } from "../../utils/vehiclePhysics/vehicleMath";
import { CONTACT_REF_EXTENT } from "../contacts/ContactPoint";
import type { ContactManifold } from "../contacts/ContactManifold";
import type { ContactPoint } from "../contacts/ContactPoint";
import type { ImpulseBody } from "./ImpulseBody";
/** 下列为 CONTACT_REF_EXTENT 下的基准值;求解时按特征尺寸缩放。 */
const PENETRATION_SLOP = 0.003; // 允许的残留穿透,小于此不修正
const PENETRATION_BIAS_FACTOR = 0.05; // Baumgarte 系数
const MAX_CORRECTION_VELOCITY = 0.04; // 穿透修正速度上限,避免弹飞
const SUPPORT_BIAS_Y = 0.5; // 仅对偏竖直的支撑面做法向 bias
const RESTITUTION_THRESHOLD = 0.1; // 接近静止时不再加弹性,避免微弹
const _r = new THREE.Vector3();
const _gcross = new THREE.Vector3();
const _local = new THREE.Vector3();
const _world = new THREE.Vector3();
const _invQuat = new THREE.Quaternion();
const _vel = new THREE.Vector3();
const _impulse = new THREE.Vector3();
function contactScale(body: ImpulseBody): number {
const extent = body.characteristicExtent?.() ?? CONTACT_REF_EXTENT;
return Math.max(1e-4, extent) / CONTACT_REF_EXTENT;
}
/**
* 准备法向约束的 biasVelocity。
* 含弹性目标分离速度,以及支撑面上的穿透位置修正。
*/
export function prepareNormalConstraint(
body: ImpulseBody,
manifold: ContactManifold,
point: ContactPoint,
dt: number,
): void {
point.biasVelocity = 0;
const scale = contactScale(body);
body.getVelocityAtPoint(point.worldPoint, _vel);
const vn = _vel.dot(manifold.normal);
const e = body.restitution ?? 0;
if (e > 0 && vn < -RESTITUTION_THRESHOLD * scale) {
// 目标分离速度 ≈ -e·vn;并入 bias 后 λ = -(vn - bias)·m ≈ (1+e)|vn|·m
point.biasVelocity += -e * vn;
}
if (dt <= 1e-8) return;
// 墙面等非支撑接触不做穿透 bias,减少侧向拉扯
if (manifold.normal.y <= SUPPORT_BIAS_Y) return;
const error = point.penetration - PENETRATION_SLOP * scale;
if (error <= 0) return;
point.biasVelocity += Math.min(
error * PENETRATION_BIAS_FACTOR / dt,
MAX_CORRECTION_VELOCITY * scale,
);
}
/** 热启动:施加上一帧累计的法向冲量。 */
export function warmStartNormal(
body: ImpulseBody,
manifold: ContactManifold,
point: ContactPoint,
): void {
if (point.normalImpulse === 0) return;
_impulse.copy(manifold.normal).multiplyScalar(point.normalImpulse);
body.applyImpulseAtPoint(_impulse, point.worldPoint);
}
/**
* 解法向非穿透约束。
* 冲量累计且钳制为 ≥ 0(只推开、不拉回)。
*/
export function solveNormalConstraint(
body: ImpulseBody,
manifold: ContactManifold,
point: ContactPoint,
): void {
const n = manifold.normal;
body.getVelocityAtPoint(point.worldPoint, _vel);
const vn = _vel.dot(n);
const mass = effectiveMass(body, point.worldPoint, n, _r, _gcross, _local, _world, _invQuat);
if (mass <= 0) return;
// 使 vn 趋向 biasVelocity(弹性 + 穿透修正)
const lambda = -(vn - point.biasVelocity) * mass;
const old = point.normalImpulse;
point.normalImpulse = Math.max(0, old + lambda);
const applied = point.normalImpulse - old;
if (applied === 0) return;
_impulse.copy(n).multiplyScalar(applied);
body.applyImpulseAtPoint(_impulse, point.worldPoint);
}