Files
gta6/three-player-controller/example/shooting/player/spineIK.js
T
knsoandClaude Opus 5 fb38652e7f 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>
2026-09-15 21:27:03 +02:00

106 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}