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,105 @@
import { Euler, Quaternion, Vector3 } from "three";
// 复用的临时量(避免每帧 GC)
const _aimQ = new Quaternion();
const _yawQ = new Quaternion();
const _localPitchQ = new Quaternion();
const _parentWorldQ = new Quaternion();
const _cameraWorldQ = new Quaternion();
const _aimAxis = new Vector3(1, 0, 0);
const _yawAxis = new Vector3(0, 1, 0);
const _headWorldQ = new Quaternion();
const _headParentWorldQ = new Quaternion();
const _headEuler = new Euler();
// 脊椎骨骼 IK
export class SpineIK {
constructor(spineBones, headBone) {
// ==================== 骨骼引用 ====================
this.spineBones = spineBones; // 脊椎骨骼数组(从下到上)
this.headBone = headBone; // 头部骨骼
// 每帧 IK 应用前保存的"干净动画姿态",下一帧先恢复再驱动
this.baseQuats = spineBones.map(() => new Quaternion());
}
// ==================== 骨骼保存 / 恢复 ====================
// 将脊椎骨骼恢复到上一帧保存的干净动画姿态
// 在 player.update()(动画驱动)之前调用,防止 IK 残留影响动画混合
restoreBones() {
for (let i = 0; i < this.spineBones.length; i++) {
this.spineBones[i].quaternion.copy(this.baseQuats[i]);
}
}
// 清除头骨骼的横滚(roll),防止第一人称下相机随动画倾斜
clearHeadRoll() {
const head = this.headBone;
if (!head) return;
head.updateWorldMatrix(true, false);
head.getWorldQuaternion(_headWorldQ);
_headEuler.setFromQuaternion(_headWorldQ, "YXZ");
_headEuler.z = 0;
_headWorldQ.setFromEuler(_headEuler);
head.parent.getWorldQuaternion(_headParentWorldQ);
head.quaternion.copy(_headParentWorldQ).invert().multiply(_headWorldQ);
head.updateWorldMatrix(false, false);
}
// ==================== IK 应用 ====================
// 第一人称脊椎俯仰 IK
// 将 pitchTarget 均分到每根脊椎骨骼,使持枪手臂跟随鼠标上下瞄准
applyAim1P(camera, pitchTarget) {
if (!this.spineBones.length) return;
this.clearHeadRoll();
camera.getWorldQuaternion(_cameraWorldQ);
_aimAxis.set(1, 0, 0).applyQuaternion(_cameraWorldQ);
_aimQ.setFromAxisAngle(_aimAxis, pitchTarget / this.spineBones.length);
this.spineBones[0].parent.updateWorldMatrix(true, false);
for (let i = 0; i < this.spineBones.length; i++) {
this.baseQuats[i].copy(this.spineBones[i].quaternion); // 保存干净姿态
// 转到父骨骼局部空间后叠加
this.spineBones[i].parent.getWorldQuaternion(_parentWorldQ);
_localPitchQ.copy(_parentWorldQ).invert().multiply(_aimQ).multiply(_parentWorldQ);
this.spineBones[i].quaternion.premultiply(_localPitchQ);
this.spineBones[i].updateWorldMatrix(false, false);
}
// 传播到头骨骼,让相机挂点跟随
this.spineBones[this.spineBones.length - 1].updateWorldMatrix(false, true);
}
// 第三人称脊椎俯仰 + 偏航 IK,同时对 camera.rotation.x 做视觉补偿偏移
applyAim3P(camera, isGunEngaged) {
if (!this.spineBones.length) return;
// 归一化俯仰(-1 ~ +1),用于二次曲线补偿
const normalizedPitch = camera.rotation.x / (Math.PI * (50 / 180));
const pitchSq = Math.pow(Math.abs(normalizedPitch), 2.0);
const pitchTarget = isGunEngaged ? camera.rotation.x : 0;
// 相机视角补偿:向下看时额外抬高,向上看时额外压低,使瞄准更自然
camera.rotation.x += normalizedPitch > 0 ? pitchSq * 0.35 : -pitchSq * 0.1;
// 人物朝向补偿:需要往屏幕中心方向旋转
const yawTarget = isGunEngaged ? -Math.PI * ((10 * (1 + pitchSq * 0.35)) / 180) : 0;
_aimAxis.set(1, 0, 0).applyQuaternion(camera.quaternion);
_aimQ.setFromAxisAngle(_aimAxis, pitchTarget / this.spineBones.length);
_yawQ.setFromAxisAngle(_yawAxis, yawTarget);
_aimQ.premultiply(_yawQ);
this.spineBones[0].parent.updateWorldMatrix(true, false);
for (let i = 0; i < this.spineBones.length; i++) {
this.baseQuats[i].copy(this.spineBones[i].quaternion);
this.spineBones[i].parent.getWorldQuaternion(_parentWorldQ);
_localPitchQ.copy(_parentWorldQ).invert().multiply(_aimQ).multiply(_parentWorldQ);
this.spineBones[i].quaternion.premultiply(_localPitchQ);
this.spineBones[i].updateWorldMatrix(false, false);
}
}
}