model.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import * as THREE from "three";
  2. import { VRM, VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
  3. import { GLTFLoader, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
  4. import { VRMAnimation } from "@/lib/VRMAnimation/VRMAnimation";
  5. import { VRMLookAtSmootherLoaderPlugin } from "@/lib/VRMLookAtSmootherLoaderPlugin/VRMLookAtSmootherLoaderPlugin";
  6. import { EmoteController } from "@/features/emoteController/emoteController";
  7. import { LipSync } from "@/features/lipSync/lipSync";
  8. import { Screenplay } from "@/features/chat/messages";
  9. /**
  10. * 3Dキャラクターを管理するクラス
  11. */
  12. export class Model {
  13. public vrm?: VRM | null;
  14. public mixer?: THREE.AnimationMixer;
  15. public emoteController?: EmoteController;
  16. private _lookAtTargetParent: THREE.Object3D;
  17. private _lipSync?: LipSync;
  18. public _currentAction?: THREE.AnimationAction;
  19. constructor(lookAtTargetParent: THREE.Object3D) {
  20. this._lookAtTargetParent = lookAtTargetParent;
  21. this._lipSync = new LipSync(new AudioContext());
  22. }
  23. public async loadVRM(url: string): Promise<void> {
  24. const loader = new GLTFLoader();
  25. // used for debug rendering
  26. const helperRoot = new THREE.Group();
  27. helperRoot.renderOrder = 10000;
  28. loader.register((parser: GLTFParser) => {
  29. const options: any = {
  30. lookAtPlugin: new VRMLookAtSmootherLoaderPlugin(parser),
  31. };
  32. return new VRMLoaderPlugin(parser, options);
  33. });
  34. const gltf = await loader.loadAsync(url);
  35. const vrm = (this.vrm = gltf.userData.vrm);
  36. vrm.scene.name = "VRMRoot";
  37. this.mixer = new THREE.AnimationMixer(vrm.scene);
  38. this.emoteController = new EmoteController(vrm, this._lookAtTargetParent);
  39. // TODO this causes helperRoot to be rendered to side
  40. VRMUtils.rotateVRM0(vrm);
  41. }
  42. public unLoadVrm() {
  43. if (this.vrm) {
  44. VRMUtils.deepDispose(this.vrm.scene);
  45. this.vrm = null;
  46. }
  47. }
  48. /**
  49. * VRMアニメーションを読み込む
  50. *
  51. * https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm_animation-1.0/README.ja.md
  52. */
  53. public async loadAnimation(
  54. animation: VRMAnimation | THREE.AnimationClip,
  55. ): Promise<void> {
  56. const { vrm, mixer } = this;
  57. if (vrm == null || mixer == null) {
  58. throw new Error("You have to load VRM first");
  59. }
  60. const clip =
  61. animation instanceof THREE.AnimationClip
  62. ? animation
  63. : animation.createAnimationClip(vrm);
  64. mixer.stopAllAction();
  65. this._currentAction = mixer.clipAction(clip);
  66. this._currentAction.play();
  67. }
  68. private async fadeToAction( destAction: THREE.AnimationAction, duration: number) {
  69. let previousAction = this._currentAction;
  70. this._currentAction = destAction;
  71. if (previousAction !== this._currentAction) {
  72. previousAction?.fadeOut(duration);
  73. }
  74. this._currentAction
  75. .reset()
  76. .setEffectiveTimeScale( 1 )
  77. .setEffectiveWeight( 1 )
  78. .fadeIn( 0.5 )
  79. .play();
  80. }
  81. private async modifyAnimationPosition(clip: THREE.AnimationClip, weight: { [key: string]: number }) {
  82. const { vrm } = this;
  83. if (vrm == null) {
  84. throw new Error("You have to load VRM first");
  85. }
  86. // Find the hips bone
  87. const hipsBone = vrm.humanoid.getNormalizedBoneNode("hips");
  88. if (!hipsBone) {
  89. throw new Error("Bone not found in VRM model");
  90. }
  91. // Use the current hips bone position as the start position
  92. const currentHipsPosition = hipsBone!.getWorldPosition(new THREE.Vector3());
  93. // Extract the start position from the animation clip
  94. let clipStartPositionHips: THREE.Vector3 | null = null;
  95. for (const track of clip.tracks) {
  96. if (track.name.endsWith(".position") && track.name.includes("Hips")) {
  97. const values = (track as THREE.VectorKeyframeTrack).values;
  98. clipStartPositionHips = new THREE.Vector3(values[0], values[1], values[2]);
  99. break;
  100. }
  101. }
  102. if (clipStartPositionHips) {
  103. // Calculate the offset
  104. const offsetHipsPosition = currentHipsPosition.clone().sub(clipStartPositionHips);
  105. // Apply the offset to all keyframes
  106. for (const track of clip.tracks) {
  107. if (track.name.endsWith(".position") && track.name.includes("Hips")) {
  108. const values = (track as THREE.VectorKeyframeTrack).values;
  109. for (let i = 0; i < values.length; i += 3) {
  110. values[i] -= offsetHipsPosition.x / weight.x;
  111. values[i + 1] += offsetHipsPosition.y * weight.y;
  112. values[i + 2] += offsetHipsPosition.z * weight.z;
  113. }
  114. }
  115. }
  116. } else {
  117. console.warn("Could not determine start position from animation clip.");
  118. }
  119. }
  120. public async playAnimation(animation: VRMAnimation | THREE.AnimationClip, name: string): Promise<number> {
  121. const { vrm, mixer } = this;
  122. if (vrm == null || mixer == null) {
  123. throw new Error("You have to load VRM first");
  124. }
  125. const clip =
  126. animation instanceof THREE.AnimationClip
  127. ? animation
  128. : animation.createAnimationClip(vrm);
  129. // modify the initial position of the VRMA animation to be sync with idle animation
  130. let weight: { [key: string]: number } = { x: 1, y: 1, z: 1 };
  131. if (!(name === "idle_loop.vrma" || name === "greeting.vrma")) {
  132. if (name === "dance.vrma") {
  133. weight = { x:2 ,y:1.25 ,z:1.5 };
  134. } else {
  135. weight = { x:1 ,y:1 ,z:0 };
  136. }
  137. this.modifyAnimationPosition(clip, weight);
  138. }
  139. const idleAction = this._currentAction!;
  140. const VRMAaction = mixer.clipAction(clip);
  141. VRMAaction.clampWhenFinished = true;
  142. VRMAaction.loop = THREE.LoopOnce;
  143. this.fadeToAction(VRMAaction,1);
  144. const restoreState = () => {
  145. mixer.removeEventListener( 'finished', restoreState );
  146. this.fadeToAction(idleAction,1);
  147. }
  148. mixer.addEventListener("finished", restoreState);
  149. return clip.duration + 1 + 0.5; // 1 = fade out time, 0.5 = fade in time
  150. }
  151. public async playEmotion(expression: string) {
  152. this.emoteController?.playEmotion(expression);
  153. }
  154. /**
  155. * 音声を再生し、リップシンクを行う
  156. */
  157. public async speak(buffer: ArrayBuffer, screenplay: Screenplay) {
  158. this.emoteController?.playEmotion(screenplay.expression);
  159. await new Promise((resolve) => {
  160. this._lipSync?.playFromArrayBuffer(buffer, () => {
  161. resolve(true);
  162. });
  163. });
  164. }
  165. public update(delta: number): void {
  166. if (this._lipSync) {
  167. const { volume } = this._lipSync.update();
  168. this.emoteController?.lipSync("aa", volume);
  169. }
  170. this.emoteController?.update(delta);
  171. this.mixer?.update(delta);
  172. this.vrm?.update(delta);
  173. }
  174. }