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