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>
106 lines
4.6 KiB
JavaScript
106 lines
4.6 KiB
JavaScript
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);
|
||
}
|
||
}
|
||
}
|