1
0

VRMLookAtSmoother.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { VRMHumanoid, VRMLookAt, VRMLookAtApplier } from "@pixiv/three-vrm";
  2. import * as THREE from "three";
  3. /** サッケードが発生するまでの最小間隔 */
  4. const SACCADE_MIN_INTERVAL = 0.5;
  5. /**
  6. * サッケードが発生する確率
  7. */
  8. const SACCADE_PROC = 0.05;
  9. /** サッケードの範囲半径。lookAtに渡される値で、実際の眼球の移動半径ではないので、若干大きめに。 in degrees */
  10. const SACCADE_RADIUS = 5.0;
  11. const _v3A = new THREE.Vector3();
  12. const _quatA = new THREE.Quaternion();
  13. const _eulerA = new THREE.Euler();
  14. /**
  15. * `VRMLookAt` に以下の機能を追加する:
  16. *
  17. * - `userTarget` がアサインされている場合、ユーザ方向にスムージングしながら向く
  18. * - 目だけでなく、頭の回転でも向く
  19. * - 眼球のサッケード運動を追加する
  20. */
  21. export class VRMLookAtSmoother extends VRMLookAt {
  22. /** スムージング用の係数 */
  23. public smoothFactor = 4.0;
  24. /** ユーザ向きに向く限界の角度 in degree */
  25. public userLimitAngle = 90.0;
  26. /** ユーザへの向き。もともと存在する `target` はアニメーションに使う */
  27. public userTarget?: THREE.Object3D | null;
  28. /** `false` にするとサッケードを無効にできます */
  29. public enableSaccade: boolean;
  30. /** サッケードの移動方向を格納しておく */
  31. private _saccadeYaw = 0.0;
  32. /** サッケードの移動方向を格納しておく */
  33. private _saccadePitch = 0.0;
  34. /** このタイマーが SACCADE_MIN_INTERVAL を超えたら SACCADE_PROC の確率でサッケードを発生させる */
  35. private _saccadeTimer = 0.0;
  36. /** スムージングするyaw */
  37. private _yawDamped = 0.0;
  38. /** スムージングするpitch */
  39. private _pitchDamped = 0.0;
  40. /** firstPersonBoneの回転を一時的にしまっておくやつ */
  41. private _tempFirstPersonBoneQuat = new THREE.Quaternion();
  42. public constructor(humanoid: VRMHumanoid, applier: VRMLookAtApplier) {
  43. super(humanoid, applier);
  44. this.enableSaccade = true;
  45. }
  46. public update(delta: number): void {
  47. if (this.target && this.autoUpdate) {
  48. // アニメーションの視線
  49. // `_yaw` と `_pitch` のアップデート
  50. this.lookAt(this.target.getWorldPosition(_v3A));
  51. // アニメーションによって指定されたyaw / pitch。この関数内で不変
  52. const yawAnimation = this._yaw;
  53. const pitchAnimation = this._pitch;
  54. // このフレームで最終的に使うことになるyaw / pitch
  55. let yawFrame = yawAnimation;
  56. let pitchFrame = pitchAnimation;
  57. // ユーザ向き
  58. if (this.userTarget) {
  59. // `_yaw` と `_pitch` のアップデート
  60. this.lookAt(this.userTarget.getWorldPosition(_v3A));
  61. // 角度の制限。 `userLimitAngle` を超えていた場合はアニメーションで指定された方向を向く
  62. if (
  63. this.userLimitAngle < Math.abs(this._yaw) ||
  64. this.userLimitAngle < Math.abs(this._pitch)
  65. ) {
  66. this._yaw = yawAnimation;
  67. this._pitch = pitchAnimation;
  68. }
  69. // yawDamped / pitchDampedをスムージングする
  70. const k = 1.0 - Math.exp(-this.smoothFactor * delta);
  71. this._yawDamped += (this._yaw - this._yawDamped) * k;
  72. this._pitchDamped += (this._pitch - this._pitchDamped) * k;
  73. // アニメーションとブレンディングする
  74. // アニメーションが横とかを向いている場合はそっちを尊重する
  75. const userRatio =
  76. 1.0 -
  77. THREE.MathUtils.smoothstep(
  78. Math.sqrt(
  79. yawAnimation * yawAnimation + pitchAnimation * pitchAnimation
  80. ),
  81. 30.0,
  82. 90.0
  83. );
  84. // yawFrame / pitchFrame に結果を代入
  85. yawFrame = THREE.MathUtils.lerp(
  86. yawAnimation,
  87. 0.6 * this._yawDamped,
  88. userRatio
  89. );
  90. pitchFrame = THREE.MathUtils.lerp(
  91. pitchAnimation,
  92. 0.6 * this._pitchDamped,
  93. userRatio
  94. );
  95. // 頭も回す
  96. _eulerA.set(
  97. -this._pitchDamped * THREE.MathUtils.DEG2RAD,
  98. this._yawDamped * THREE.MathUtils.DEG2RAD,
  99. 0.0,
  100. VRMLookAt.EULER_ORDER
  101. );
  102. _quatA.setFromEuler(_eulerA);
  103. const head = this.humanoid.getRawBoneNode("head")!;
  104. this._tempFirstPersonBoneQuat.copy(head.quaternion);
  105. head.quaternion.slerp(_quatA, 0.4);
  106. head.updateMatrixWorld();
  107. }
  108. if (this.enableSaccade) {
  109. // サッケードの移動方向を計算
  110. if (
  111. SACCADE_MIN_INTERVAL < this._saccadeTimer &&
  112. Math.random() < SACCADE_PROC
  113. ) {
  114. this._saccadeYaw = (2.0 * Math.random() - 1.0) * SACCADE_RADIUS;
  115. this._saccadePitch = (2.0 * Math.random() - 1.0) * SACCADE_RADIUS;
  116. this._saccadeTimer = 0.0;
  117. }
  118. this._saccadeTimer += delta;
  119. // サッケードの移動分を加算
  120. yawFrame += this._saccadeYaw;
  121. pitchFrame += this._saccadePitch;
  122. // applierにわたす
  123. this.applier.applyYawPitch(yawFrame, pitchFrame);
  124. }
  125. // applyはもうしたので、このフレーム内でアップデートする必要はない
  126. this._needsUpdate = false;
  127. }
  128. // targetでlookAtを制御しない場合
  129. if (this._needsUpdate) {
  130. this._needsUpdate = false;
  131. this.applier.applyYawPitch(this._yaw, this._pitch);
  132. }
  133. }
  134. /** renderしたあとに叩いて頭の回転をもとに戻す */
  135. public revertFirstPersonBoneQuat(): void {
  136. if (this.userTarget) {
  137. const head = this.humanoid.getNormalizedBoneNode("head")!;
  138. head.quaternion.copy(this._tempFirstPersonBoneQuat);
  139. }
  140. }
  141. }