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