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:
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);
|
||||
}
|
||||
Reference in new issue
Block a user