@memori.ai/memori-react 7.7.1 → 7.8.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/components/Avatar/Avatar.js +2 -2
  3. package/dist/components/Avatar/Avatar.js.map +1 -1
  4. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.d.ts +6 -3
  5. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +10 -6
  6. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -1
  7. package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.d.ts +13 -17
  8. package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js +110 -111
  9. package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js.map +1 -1
  10. package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.d.ts +3 -4
  11. package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js +77 -52
  12. package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js.map +1 -1
  13. package/dist/components/Avatar/AvatarView/index.d.ts +7 -3
  14. package/dist/components/Avatar/AvatarView/index.js +2 -2
  15. package/dist/components/Avatar/AvatarView/index.js.map +1 -1
  16. package/dist/components/MemoriWidget/MemoriWidget.js +34 -22
  17. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  18. package/dist/components/layouts/ZoomedFullBody.js +1 -8
  19. package/dist/components/layouts/ZoomedFullBody.js.map +1 -1
  20. package/dist/components/layouts/zoomed-full-body.css +5 -1
  21. package/dist/context/visemeContext.d.ts +9 -15
  22. package/dist/context/visemeContext.js +73 -190
  23. package/dist/context/visemeContext.js.map +1 -1
  24. package/esm/components/Avatar/Avatar.js +2 -2
  25. package/esm/components/Avatar/Avatar.js.map +1 -1
  26. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.d.ts +6 -3
  27. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +10 -6
  28. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -1
  29. package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.d.ts +13 -17
  30. package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js +112 -113
  31. package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js.map +1 -1
  32. package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.d.ts +3 -4
  33. package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js +81 -55
  34. package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js.map +1 -1
  35. package/esm/components/Avatar/AvatarView/index.d.ts +7 -3
  36. package/esm/components/Avatar/AvatarView/index.js +2 -2
  37. package/esm/components/Avatar/AvatarView/index.js.map +1 -1
  38. package/esm/components/MemoriWidget/MemoriWidget.js +34 -22
  39. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  40. package/esm/components/layouts/ZoomedFullBody.js +1 -8
  41. package/esm/components/layouts/ZoomedFullBody.js.map +1 -1
  42. package/esm/components/layouts/zoomed-full-body.css +5 -1
  43. package/esm/context/visemeContext.d.ts +9 -15
  44. package/esm/context/visemeContext.js +74 -191
  45. package/esm/context/visemeContext.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/components/Avatar/Avatar.tsx +5 -5
  48. package/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx +33 -19
  49. package/src/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.tsx +181 -158
  50. package/src/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.tsx +116 -71
  51. package/src/components/Avatar/AvatarView/AvatarView.stories.tsx +12 -24
  52. package/src/components/Avatar/AvatarView/index.tsx +9 -8
  53. package/src/components/MemoriWidget/MemoriWidget.tsx +45 -38
  54. package/src/components/layouts/ZoomedFullBody.tsx +6 -11
  55. package/src/components/layouts/zoomed-full-body.css +5 -1
  56. package/src/context/visemeContext.tsx +130 -283
  57. package/dist/components/AttachmentMediaModal/AttachmentMediaModal.d.ts +0 -14
  58. package/dist/components/AttachmentMediaModal/AttachmentMediaModal.js +0 -66
  59. package/dist/components/AttachmentMediaModal/AttachmentMediaModal.js.map +0 -1
  60. package/dist/components/Avatar/AvatarView/components/controls.d.ts +0 -23
  61. package/dist/components/Avatar/AvatarView/components/controls.js +0 -55
  62. package/dist/components/Avatar/AvatarView/components/controls.js.map +0 -1
  63. package/dist/components/Avatar/AvatarView/components/fullbodyAvatar.d.ts +0 -19
  64. package/dist/components/Avatar/AvatarView/components/fullbodyAvatar.js +0 -61
  65. package/dist/components/Avatar/AvatarView/components/fullbodyAvatar.js.map +0 -1
  66. package/dist/components/Avatar/AvatarView/components/halfbodyAvatar.d.ts +0 -9
  67. package/dist/components/Avatar/AvatarView/components/halfbodyAvatar.js +0 -39
  68. package/dist/components/Avatar/AvatarView/components/halfbodyAvatar.js.map +0 -1
  69. package/dist/components/Avatar/AvatarView/components/loader.d.ts +0 -5
  70. package/dist/components/Avatar/AvatarView/components/loader.js +0 -12
  71. package/dist/components/Avatar/AvatarView/components/loader.js.map +0 -1
  72. package/dist/components/Avatar/AvatarView/utils/useMouthSpeaking.d.ts +0 -2
  73. package/dist/components/Avatar/AvatarView/utils/useMouthSpeaking.js +0 -68
  74. package/dist/components/Avatar/AvatarView/utils/useMouthSpeaking.js.map +0 -1
  75. package/dist/components/AvatarView/components/avatar.d.ts +0 -9
  76. package/dist/components/AvatarView/components/avatar.js +0 -35
  77. package/dist/components/AvatarView/components/avatar.js.map +0 -1
  78. package/dist/components/AvatarView/components/fullbodyAvatar.d.ts +0 -10
  79. package/dist/components/AvatarView/components/fullbodyAvatar.js +0 -62
  80. package/dist/components/AvatarView/components/fullbodyAvatar.js.map +0 -1
  81. package/dist/components/AvatarView/components/loader.d.ts +0 -5
  82. package/dist/components/AvatarView/components/loader.js +0 -12
  83. package/dist/components/AvatarView/components/loader.js.map +0 -1
  84. package/dist/components/AvatarView/index.d.ts +0 -17
  85. package/dist/components/AvatarView/index.js +0 -35
  86. package/dist/components/AvatarView/index.js.map +0 -1
  87. package/dist/components/AvatarView/utils/useEyeBlink.d.ts +0 -2
  88. package/dist/components/AvatarView/utils/useEyeBlink.js +0 -40
  89. package/dist/components/AvatarView/utils/useEyeBlink.js.map +0 -1
  90. package/dist/components/AvatarView/utils/useHeadMovement.d.ts +0 -2
  91. package/dist/components/AvatarView/utils/useHeadMovement.js +0 -53
  92. package/dist/components/AvatarView/utils/useHeadMovement.js.map +0 -1
  93. package/dist/components/AvatarView/utils/useLoadingMorphAnim.d.ts +0 -2
  94. package/dist/components/AvatarView/utils/useLoadingMorphAnim.js +0 -34
  95. package/dist/components/AvatarView/utils/useLoadingMorphAnim.js.map +0 -1
  96. package/dist/components/AvatarView/utils/useMouthSpeaking.d.ts +0 -2
  97. package/dist/components/AvatarView/utils/useMouthSpeaking.js +0 -60
  98. package/dist/components/AvatarView/utils/useMouthSpeaking.js.map +0 -1
  99. package/dist/components/AvatarView/utils/useSmile.d.ts +0 -2
  100. package/dist/components/AvatarView/utils/useSmile.js +0 -30
  101. package/dist/components/AvatarView/utils/useSmile.js.map +0 -1
  102. package/dist/components/AvatarView/utils/utils.d.ts +0 -13
  103. package/dist/components/AvatarView/utils/utils.js +0 -42
  104. package/dist/components/AvatarView/utils/utils.js.map +0 -1
  105. package/dist/components/ImageUpload/ImageUpload.css +0 -168
  106. package/dist/components/ImageUpload/ImageUpload.d.ts +0 -28
  107. package/dist/components/ImageUpload/ImageUpload.js +0 -163
  108. package/dist/components/ImageUpload/ImageUpload.js.map +0 -1
  109. package/dist/components/layouts/Default.d.ts +0 -17
  110. package/dist/components/layouts/Default.js +0 -8
  111. package/dist/components/layouts/Default.js.map +0 -1
  112. package/dist/components/ui/Message.d.ts +0 -17
  113. package/dist/components/ui/Message.js +0 -13
  114. package/dist/components/ui/Message.js.map +0 -1
  115. package/esm/components/AttachmentMediaModal/AttachmentMediaModal.d.ts +0 -14
  116. package/esm/components/AttachmentMediaModal/AttachmentMediaModal.js +0 -63
  117. package/esm/components/AttachmentMediaModal/AttachmentMediaModal.js.map +0 -1
  118. package/esm/components/Avatar/AvatarView/components/controls.d.ts +0 -23
  119. package/esm/components/Avatar/AvatarView/components/controls.js +0 -52
  120. package/esm/components/Avatar/AvatarView/components/controls.js.map +0 -1
  121. package/esm/components/Avatar/AvatarView/components/fullbodyAvatar.d.ts +0 -19
  122. package/esm/components/Avatar/AvatarView/components/fullbodyAvatar.js +0 -57
  123. package/esm/components/Avatar/AvatarView/components/fullbodyAvatar.js.map +0 -1
  124. package/esm/components/Avatar/AvatarView/components/halfbodyAvatar.d.ts +0 -9
  125. package/esm/components/Avatar/AvatarView/components/halfbodyAvatar.js +0 -35
  126. package/esm/components/Avatar/AvatarView/components/halfbodyAvatar.js.map +0 -1
  127. package/esm/components/Avatar/AvatarView/components/loader.d.ts +0 -5
  128. package/esm/components/Avatar/AvatarView/components/loader.js +0 -9
  129. package/esm/components/Avatar/AvatarView/components/loader.js.map +0 -1
  130. package/esm/components/Avatar/AvatarView/utils/useMouthSpeaking.d.ts +0 -2
  131. package/esm/components/Avatar/AvatarView/utils/useMouthSpeaking.js +0 -65
  132. package/esm/components/Avatar/AvatarView/utils/useMouthSpeaking.js.map +0 -1
  133. package/esm/components/AvatarView/components/avatar.d.ts +0 -9
  134. package/esm/components/AvatarView/components/avatar.js +0 -31
  135. package/esm/components/AvatarView/components/avatar.js.map +0 -1
  136. package/esm/components/AvatarView/components/fullbodyAvatar.d.ts +0 -10
  137. package/esm/components/AvatarView/components/fullbodyAvatar.js +0 -58
  138. package/esm/components/AvatarView/components/fullbodyAvatar.js.map +0 -1
  139. package/esm/components/AvatarView/components/loader.d.ts +0 -5
  140. package/esm/components/AvatarView/components/loader.js +0 -9
  141. package/esm/components/AvatarView/components/loader.js.map +0 -1
  142. package/esm/components/AvatarView/index.d.ts +0 -17
  143. package/esm/components/AvatarView/index.js +0 -31
  144. package/esm/components/AvatarView/index.js.map +0 -1
  145. package/esm/components/AvatarView/utils/useEyeBlink.d.ts +0 -2
  146. package/esm/components/AvatarView/utils/useEyeBlink.js +0 -37
  147. package/esm/components/AvatarView/utils/useEyeBlink.js.map +0 -1
  148. package/esm/components/AvatarView/utils/useHeadMovement.d.ts +0 -2
  149. package/esm/components/AvatarView/utils/useHeadMovement.js +0 -50
  150. package/esm/components/AvatarView/utils/useHeadMovement.js.map +0 -1
  151. package/esm/components/AvatarView/utils/useLoadingMorphAnim.d.ts +0 -2
  152. package/esm/components/AvatarView/utils/useLoadingMorphAnim.js +0 -31
  153. package/esm/components/AvatarView/utils/useLoadingMorphAnim.js.map +0 -1
  154. package/esm/components/AvatarView/utils/useMouthSpeaking.d.ts +0 -2
  155. package/esm/components/AvatarView/utils/useMouthSpeaking.js +0 -57
  156. package/esm/components/AvatarView/utils/useMouthSpeaking.js.map +0 -1
  157. package/esm/components/AvatarView/utils/useSmile.d.ts +0 -2
  158. package/esm/components/AvatarView/utils/useSmile.js +0 -27
  159. package/esm/components/AvatarView/utils/useSmile.js.map +0 -1
  160. package/esm/components/AvatarView/utils/utils.d.ts +0 -13
  161. package/esm/components/AvatarView/utils/utils.js +0 -33
  162. package/esm/components/AvatarView/utils/utils.js.map +0 -1
  163. package/esm/components/ImageUpload/ImageUpload.css +0 -168
  164. package/esm/components/ImageUpload/ImageUpload.d.ts +0 -28
  165. package/esm/components/ImageUpload/ImageUpload.js +0 -160
  166. package/esm/components/ImageUpload/ImageUpload.js.map +0 -1
  167. package/esm/components/layouts/Default.d.ts +0 -17
  168. package/esm/components/layouts/Default.js +0 -5
  169. package/esm/components/layouts/Default.js.map +0 -1
  170. package/esm/components/ui/Message.d.ts +0 -17
  171. package/esm/components/ui/Message.js +0 -10
  172. package/esm/components/ui/Message.js.map +0 -1
@@ -1,22 +1,17 @@
1
- import React, { useEffect, useRef, useState, useCallback } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  Vector3,
4
4
  Euler,
5
5
  AnimationMixer,
6
6
  SkinnedMesh,
7
7
  Object3D,
8
+ MathUtils,
8
9
  AnimationAction,
9
10
  LoopOnce,
10
11
  } from 'three';
11
12
  import { useAnimations, useGLTF } from '@react-three/drei';
12
13
  import { useGraph, useFrame } from '@react-three/fiber';
13
14
  import { correctMaterials, isSkinnedMesh } from '../../../../../helpers/utils';
14
- import { useAvatarBlink } from '../../utils/useEyeBlink';
15
- import { useViseme } from '../../../../../context/visemeContext';
16
-
17
- const lerp = (start: number, end: number, alpha: number): number => {
18
- return start * (1 - alpha) + end * alpha;
19
- };
20
15
 
21
16
  interface FullbodyAvatarProps {
22
17
  url: string;
@@ -27,16 +22,23 @@ interface FullbodyAvatarProps {
27
22
  weight: number;
28
23
  };
29
24
  timeScale: number;
30
- loading?: boolean;
31
- speaking?: boolean;
32
25
  isZoomed?: boolean;
33
- setMorphTargetInfluences: (influences: { [key: string]: number }) => void;
34
- setMorphTargetDictionary: (dictionary: { [key: string]: number }) => void;
35
- morphTargetInfluences: { [key: string]: number };
36
- morphTargetDictionary: { [key: string]: number };
37
- setMeshRef: any;
38
26
  eyeBlink?: boolean;
39
- clearVisemes: () => void;
27
+ stopProcessing: () => void;
28
+ resetVisemeQueue: () => void;
29
+ updateCurrentViseme: (
30
+ currentTime: number
31
+ ) => { name: string; weight: number } | null;
32
+ smoothMorphTarget?: boolean;
33
+ morphTargetSmoothing?: number;
34
+ morphTargetInfluences: Record<string, number>;
35
+ setMorphTargetDictionary: (
36
+ morphTargetDictionary: Record<string, number>
37
+ ) => void;
38
+ setMorphTargetInfluences: (
39
+ morphTargetInfluences: Record<string, number>
40
+ ) => void;
41
+ emotionMorphTargets: Record<string, number>;
40
42
  }
41
43
 
42
44
  const AVATAR_POSITION = new Vector3(0, -1, 0);
@@ -49,7 +51,14 @@ const ANIMATION_URLS = {
49
51
  'https://assets.memori.ai/api/v2/asset/0e49aa5d-f757-4292-a170-d843c2839a41.glb',
50
52
  };
51
53
 
52
- const TRANSITION_DURATION = 0.5; // Duration for transitioning between animations
54
+ // Blink configuration
55
+ const BLINK_CONFIG = {
56
+ minInterval: 1000,
57
+ maxInterval: 5000,
58
+ blinkDuration: 150,
59
+ };
60
+
61
+ const EMOTION_TRANSITION_SPEED = 0.1; // Adjust this value to control emotion transition speed
53
62
 
54
63
  export default function FullbodyAvatar({
55
64
  url,
@@ -58,92 +67,63 @@ export default function FullbodyAvatar({
58
67
  currentBaseAction,
59
68
  timeScale,
60
69
  isZoomed,
61
- setMorphTargetInfluences,
62
- setMorphTargetDictionary,
63
- morphTargetInfluences,
64
70
  eyeBlink,
65
- setMeshRef,
66
- //clearVisemes,
71
+ morphTargetSmoothing = 0.5,
72
+ updateCurrentViseme,
73
+ setMorphTargetDictionary,
74
+ setMorphTargetInfluences,
75
+ emotionMorphTargets,
67
76
  }: FullbodyAvatarProps) {
68
77
  const { scene } = useGLTF(url);
69
78
  const { animations } = useGLTF(ANIMATION_URLS[sex]);
70
79
  const { nodes, materials } = useGraph(scene);
71
80
  const { actions } = useAnimations(animations, scene);
72
- const [mixer] = useState(() => new AnimationMixer(scene));
73
81
 
74
- const avatarMeshRef = useRef<SkinnedMesh>();
82
+ const mixer = useRef(new AnimationMixer(scene));
83
+ const headMeshRef = useRef<SkinnedMesh>();
75
84
  const currentActionRef = useRef<AnimationAction | null>(null);
76
- const isTransitioningRef = useRef(false);
77
- const lastActionTimeRef = useRef(0);
78
85
  const [isTransitioningToIdle, setIsTransitioningToIdle] = useState(false);
79
- // Blink animation
80
- useAvatarBlink({
81
- enabled: eyeBlink || false,
82
- setMorphTargetInfluences,
83
- config: {
84
- minInterval: 1500,
85
- maxInterval: 4000,
86
- blinkDuration: 120,
87
- },
88
- });
89
86
 
90
- // Idle animation when emotion animation is finished
91
- const transitionToIdle = useCallback(() => {
92
- if (!actions || isTransitioningRef.current) return;
93
-
94
- isTransitioningRef.current = true;
95
-
96
- const finishCurrentAnimation = () => {
97
- if (currentActionRef.current && !currentActionRef.current.paused) {
98
- const remainingTime =
99
- (currentActionRef.current.getClip().duration -
100
- currentActionRef.current.time) *
101
- 1000;
102
- setTimeout(() => {
103
- startIdleAnimation();
104
- }, remainingTime);
105
- } else {
106
- startIdleAnimation();
107
- }
108
- };
87
+ // Blink state
88
+ const lastBlinkTime = useRef(0);
89
+ const nextBlinkTime = useRef(0);
90
+ const isBlinking = useRef(false);
91
+ const blinkStartTime = useRef(0);
109
92
 
110
- const startIdleAnimation = () => {
111
- const idleAnimations = Object.keys(actions).filter(key =>
112
- key.startsWith('Idle')
113
- );
114
- const randomIdle =
115
- idleAnimations[Math.floor(Math.random() * idleAnimations.length)];
116
-
117
- const idleAction = actions[randomIdle];
118
- if (currentActionRef.current && idleAction) {
119
- currentActionRef.current.crossFadeTo(
120
- idleAction,
121
- TRANSITION_DURATION,
122
- true
123
- );
124
- }
93
+ // Morph targets
94
+ const currentEmotionRef = useRef<Record<string, number>>({});
95
+ const previousEmotionKeysRef = useRef<Set<string>>(new Set());
96
+
97
+ useEffect(() => {
98
+ correctMaterials(materials);
125
99
 
126
- if (idleAction) {
127
- idleAction.reset().fadeIn(TRANSITION_DURATION).play();
128
- currentActionRef.current = idleAction;
100
+ scene.traverse((object: Object3D) => {
101
+ if (object instanceof SkinnedMesh) {
102
+ if (object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar') {
103
+ headMeshRef.current = object;
104
+ if (object.morphTargetDictionary && object.morphTargetInfluences) {
105
+ setMorphTargetDictionary(object.morphTargetDictionary);
106
+
107
+ const initialInfluences = Object.keys(
108
+ object.morphTargetDictionary
109
+ ).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
110
+ setMorphTargetInfluences(initialInfluences);
111
+ }
112
+ }
129
113
  }
114
+ });
130
115
 
131
- setTimeout(() => {
132
- isTransitioningRef.current = false;
133
- }, TRANSITION_DURATION * 1000);
134
- };
116
+ onLoaded?.();
135
117
 
136
- if (
137
- currentActionRef.current &&
138
- !currentActionRef.current.getClip().name.startsWith('Idle')
139
- ) {
140
- finishCurrentAnimation();
141
- } else {
142
- startIdleAnimation();
143
- }
144
- }, [actions]);
118
+ return () => {
119
+ Object.values(materials).forEach(material => material.dispose());
120
+ Object.values(nodes)
121
+ .filter(isSkinnedMesh)
122
+ .forEach(mesh => mesh.geometry.dispose());
123
+ };
124
+ }, [materials, nodes, url, onLoaded, scene]);
145
125
 
146
- // Base animation
126
+ // Handle base animation changes
147
127
  useEffect(() => {
148
128
  if (!actions || !currentBaseAction.action) return;
149
129
 
@@ -183,88 +163,131 @@ export default function FullbodyAvatar({
183
163
  }
184
164
  }, [actions, currentBaseAction, timeScale]);
185
165
 
186
- // Set up the mesh reference and morph target influences
187
- useEffect(() => {
188
- correctMaterials(materials);
166
+ useFrame(state => {
167
+ if (
168
+ headMeshRef.current &&
169
+ headMeshRef.current.morphTargetDictionary &&
170
+ headMeshRef.current.morphTargetInfluences
171
+ ) {
172
+ const currentTime = state.clock.getElapsedTime() * 1000; // Convert to milliseconds
173
+
174
+ // Handle blinking
175
+ let blinkValue = 0;
176
+ if (eyeBlink) {
177
+ if (currentTime >= nextBlinkTime.current && !isBlinking.current) {
178
+ isBlinking.current = true;
179
+ blinkStartTime.current = currentTime;
180
+ lastBlinkTime.current = currentTime;
181
+ nextBlinkTime.current =
182
+ currentTime +
183
+ Math.random() *
184
+ (BLINK_CONFIG.maxInterval - BLINK_CONFIG.minInterval) +
185
+ BLINK_CONFIG.minInterval;
186
+ }
189
187
 
190
- scene.traverse((object: Object3D) => {
191
- if (
192
- object instanceof SkinnedMesh &&
193
- (object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar')
194
- ) {
195
- avatarMeshRef.current = object;
196
- setMeshRef(object);
197
-
198
- if (object.morphTargetDictionary && object.morphTargetInfluences) {
199
- setMorphTargetDictionary(object.morphTargetDictionary);
200
-
201
- const initialInfluences = Object.keys(
202
- object.morphTargetDictionary
203
- ).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
204
- setMorphTargetInfluences(initialInfluences);
188
+ if (isBlinking.current) {
189
+ const blinkProgress =
190
+ (currentTime - blinkStartTime.current) / BLINK_CONFIG.blinkDuration;
191
+ if (blinkProgress <= 0.5) {
192
+ // Eyes closing
193
+ blinkValue = blinkProgress * 2;
194
+ } else if (blinkProgress <= 1) {
195
+ // Eyes opening
196
+ blinkValue = 2 - blinkProgress * 2;
197
+ } else {
198
+ // Blink finished
199
+ isBlinking.current = false;
200
+ blinkValue = 0;
201
+ }
205
202
  }
206
203
  }
207
- });
208
204
 
209
- onLoaded?.();
205
+ const currentViseme = updateCurrentViseme(currentTime / 1000);
206
+
207
+ // Create a set of current emotion keys
208
+ const currentEmotionKeys = new Set(Object.keys(emotionMorphTargets));
209
+
210
+ // Reset old emotion morph targets
211
+ previousEmotionKeysRef.current.forEach(key => {
212
+ if (!currentEmotionKeys.has(key)) {
213
+ const index = headMeshRef.current!.morphTargetDictionary![key];
214
+ if (typeof index === 'number') {
215
+ currentEmotionRef.current[key] = 0;
216
+ if (headMeshRef.current && headMeshRef.current.morphTargetInfluences) {
217
+ headMeshRef.current.morphTargetInfluences[index] = 0;
218
+ }
219
+ }
220
+ }
221
+ });
210
222
 
211
- return () => {
212
- Object.values(materials).forEach(material => material.dispose());
213
- Object.values(nodes)
214
- .filter(isSkinnedMesh)
215
- .forEach(mesh => mesh.geometry.dispose());
216
- mixer.stopAllAction();
217
- };
218
- }, [
219
- materials,
220
- nodes,
221
- url,
222
- onLoaded,
223
- setMorphTargetDictionary,
224
- setMorphTargetInfluences,
225
- setMeshRef,
226
- mixer,
227
- ]);
228
-
229
- // Update morph target influences
230
- useFrame((_, delta) => {
231
- if (avatarMeshRef.current && avatarMeshRef.current.morphTargetDictionary) {
232
- updateMorphTargetInfluences();
233
- }
234
- mixer.update(delta);
223
+ // Update morph targets
224
+ Object.entries(headMeshRef.current.morphTargetDictionary).forEach(
225
+ ([key, index]) => {
226
+ if (typeof index === 'number') {
227
+ let targetValue = 0;
228
+
229
+ // Handle emotions (base layer)
230
+ if (Object.prototype.hasOwnProperty.call(emotionMorphTargets, key)) {
231
+ const targetEmotionValue = emotionMorphTargets[key];
232
+ const currentEmotionValue = currentEmotionRef.current[key] || 0;
233
+ const newEmotionValue = MathUtils.lerp(
234
+ currentEmotionValue,
235
+ targetEmotionValue * 2,
236
+ EMOTION_TRANSITION_SPEED
237
+ );
238
+ currentEmotionRef.current[key] = newEmotionValue;
239
+ targetValue += newEmotionValue;
240
+ }
241
+
242
+ // Handle visemes (additive layer)
243
+ if (currentViseme && key === currentViseme.name) {
244
+ targetValue += currentViseme.weight * 1.3; // Amplify the effect
245
+ }
246
+
247
+ // Handle blinking (additive layer, only for 'eyesClosed')
248
+ if (key === 'eyesClosed' && eyeBlink) {
249
+ targetValue += blinkValue;
250
+ }
251
+
252
+ // Clamp the final value between 0 and 1
253
+ targetValue = MathUtils.clamp(targetValue, 0, 1);
254
+
255
+ // Apply smoothing
256
+ if (headMeshRef.current && headMeshRef.current.morphTargetInfluences) {
257
+ headMeshRef.current.morphTargetInfluences[index] = MathUtils.lerp(
258
+ headMeshRef.current.morphTargetInfluences[index],
259
+ targetValue,
260
+ morphTargetSmoothing
261
+ );
262
+ }
263
+ }
264
+ }
265
+ );
235
266
 
236
- function updateMorphTargetInfluences() {
237
- Object.entries(morphTargetInfluences).forEach(([key, value]) => {
238
- const index = avatarMeshRef.current!.morphTargetDictionary![key];
267
+ // Update the set of previous emotion keys for the next frame
268
+ previousEmotionKeysRef.current = currentEmotionKeys;
269
+
270
+ // Handle transition from emotion animation to idle
271
+ if (isTransitioningToIdle && currentActionRef.current) {
239
272
  if (
240
- typeof index === 'number' &&
241
- avatarMeshRef.current!.morphTargetInfluences
273
+ currentActionRef.current.time >=
274
+ currentActionRef.current.getClip().duration
242
275
  ) {
243
- const currentValue =
244
- avatarMeshRef.current!.morphTargetInfluences[index];
245
- const smoothValue = lerp(currentValue, value, 0.1);
246
- avatarMeshRef.current!.morphTargetInfluences[index] = smoothValue;
247
- }
248
- });
249
- }
250
-
251
- // Handle transition from emotion animation to idle
252
- if (isTransitioningToIdle && currentActionRef.current) {
253
- if (
254
- currentActionRef.current.time >=
255
- currentActionRef.current.getClip().duration
256
- ) {
257
- // Transition to the idle animation
258
- const idleNumber = Math.floor(Math.random() * 5) + 1; // Randomly choose 1, 2, 3, 4 or 5
259
- const idleAction = actions[`Idle${idleNumber == 3 ? 4 : idleNumber}`];
260
-
261
- if (idleAction) {
262
- currentActionRef.current.fadeOut(0.5);
263
- idleAction.reset().fadeIn(0.5).play();
264
- currentActionRef.current = idleAction;
265
- setIsTransitioningToIdle(false);
276
+ // Transition to the idle animation
277
+ const idleNumber = Math.floor(Math.random() * 5) + 1; // Randomly choose 1, 2, 3, 4 or 5
278
+ const idleAction = actions[`Idle${idleNumber == 3 ? 4 : idleNumber}`];
279
+
280
+ if (idleAction) {
281
+ currentActionRef.current.fadeOut(0.5);
282
+ idleAction.reset().fadeIn(0.5).play();
283
+ currentActionRef.current = idleAction;
284
+ setIsTransitioningToIdle(false);
285
+ }
266
286
  }
267
287
  }
288
+
289
+ // Update the animation mixer
290
+ mixer.current.update(0.01); // Fixed delta time for consistent animation speed
268
291
  }
269
292
  });
270
293
 
@@ -6,7 +6,7 @@ import { useGraph, dispose, useFrame } from '@react-three/fiber';
6
6
  import { useAvatarBlink } from '../../utils/useEyeBlink';
7
7
  import useHeadMovement from '../../utils/useHeadMovement';
8
8
  import { hideHands } from '../../utils/utils';
9
-
9
+ import { AnimationMixer, MathUtils } from 'three';
10
10
 
11
11
  interface HalfBodyAvatarProps {
12
12
  url: string;
@@ -14,110 +14,155 @@ interface HalfBodyAvatarProps {
14
14
  headMovement?: boolean;
15
15
  speaking?: boolean;
16
16
  onLoaded?: () => void;
17
- setMeshRef: (mesh: Object3D) => void;
18
- clearVisemes: () => void;
19
17
  setMorphTargetDictionary: (morphTargetDictionary: any) => void;
20
18
  eyeBlink?: boolean;
21
19
  morphTargetInfluences: any;
20
+ updateCurrentViseme: (currentTime: number) => any;
21
+ morphTargetSmoothing?: number;
22
22
  }
23
23
 
24
24
  const AVATAR_POSITION = new Vector3(0, -0.6, 0);
25
- const lerp = (start: number, end: number, alpha: number): number => {
26
- return start * (1 - alpha) + end * alpha;
25
+ // Blink configuration
26
+ const BLINK_CONFIG = {
27
+ minInterval: 1000,
28
+ maxInterval: 5000,
29
+ blinkDuration: 150,
27
30
  };
31
+
28
32
  export default function HalfBodyAvatar({
29
33
  url,
30
34
  setMorphTargetInfluences,
31
35
  setMorphTargetDictionary,
32
- headMovement,
33
36
  eyeBlink,
34
- setMeshRef,
35
37
  onLoaded,
36
- clearVisemes,
37
- morphTargetInfluences,
38
+ morphTargetSmoothing = 0.5,
39
+ updateCurrentViseme,
38
40
  }: HalfBodyAvatarProps) {
39
41
  const { scene } = useGLTF(url);
40
42
  const { nodes, materials } = useGraph(scene);
43
+ const mixer = useRef(new AnimationMixer(scene));
41
44
  const avatarMeshRef = useRef<SkinnedMesh | null>(null);
42
45
 
43
- useAvatarBlink({
44
- enabled: eyeBlink || false,
45
- setMorphTargetInfluences,
46
- config: {
47
- minInterval: 1500,
48
- maxInterval: 4000,
49
- blinkDuration: 120
50
- }
51
- });
52
- useHeadMovement(headMovement, nodes);
46
+ // Blink state
47
+ const lastBlinkTime = useRef(0);
48
+ const nextBlinkTime = useRef(0);
49
+ const isBlinking = useRef(false);
50
+ const blinkStartTime = useRef(0);
51
+
52
+ const headMeshRef = useRef<SkinnedMesh>();
53
53
 
54
54
  useEffect(() => {
55
- const setupAvatar = () => {
56
- hideHands(nodes);
57
- correctMaterials(materials);
58
- // Set mesh reference for the first SkinnedMesh found
59
- const firstSkinnedMesh = Object.values(nodes).find(isSkinnedMesh) as SkinnedMesh;
60
- if (firstSkinnedMesh) {
61
- setMeshRef(firstSkinnedMesh);
62
- avatarMeshRef.current = firstSkinnedMesh;
63
- if (firstSkinnedMesh.morphTargetDictionary && firstSkinnedMesh.morphTargetInfluences) {
64
- setMorphTargetDictionary(firstSkinnedMesh.morphTargetDictionary);
65
- const initialInfluences = Object.keys(
66
- firstSkinnedMesh.morphTargetDictionary
67
- ).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
68
- setMorphTargetInfluences(initialInfluences);
55
+ correctMaterials(materials);
56
+
57
+ scene.traverse((object: Object3D) => {
58
+ if (object instanceof SkinnedMesh) {
59
+ if (object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar') {
60
+ headMeshRef.current = object;
61
+ if (object.morphTargetDictionary && object.morphTargetInfluences) {
62
+ setMorphTargetDictionary(object.morphTargetDictionary);
63
+
64
+ const initialInfluences = Object.keys(
65
+ object.morphTargetDictionary
66
+ ).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
67
+ setMorphTargetInfluences(initialInfluences);
68
+ }
69
69
  }
70
70
  }
71
- onLoaded?.();
72
- };
71
+ });
73
72
 
74
- setupAvatar();
73
+ onLoaded?.();
75
74
 
76
75
  return () => {
77
- const disposeObjects = () => {
78
- Object.values(materials).forEach(dispose);
79
- Object.values(nodes).filter(isSkinnedMesh).forEach(dispose);
80
- clearVisemes();
81
- };
82
-
83
- disposeObjects();
76
+ Object.values(materials).forEach(material => material.dispose());
77
+ Object.values(nodes)
78
+ .filter(isSkinnedMesh)
79
+ .forEach(mesh => mesh.geometry.dispose());
84
80
  };
85
- }, [materials, nodes, url, onLoaded, clearVisemes]);
81
+ }, [materials, nodes, url, onLoaded, scene]);
82
+ useFrame(state => {
86
83
 
87
- const skinnedMeshes = useMemo(
88
- () => Object.values(nodes).filter(isSkinnedMesh),
89
- [nodes]
90
- );
84
+ if (
85
+ headMeshRef.current &&
86
+ headMeshRef.current.morphTargetDictionary &&
87
+ headMeshRef.current.morphTargetInfluences
88
+ ) {
89
+ const currentTime = state.clock.getElapsedTime() * 1000; // Convert to milliseconds
91
90
 
92
- // Update morph target influences
93
- useFrame((_) => {
94
- if (avatarMeshRef.current && avatarMeshRef.current.morphTargetDictionary) {
95
- updateMorphTargetInfluences();
96
- }
97
-
98
- function updateMorphTargetInfluences() {
99
- Object.entries(morphTargetInfluences).forEach(([key, value]) => {
100
- const index = avatarMeshRef.current!.morphTargetDictionary![key];
101
- if (typeof index === 'number' &&
102
- avatarMeshRef.current!.morphTargetInfluences) {
103
- const currentValue = avatarMeshRef.current!.morphTargetInfluences[index];
104
- const smoothValue = lerp(currentValue, value as number, 0.1);
105
- avatarMeshRef.current!.morphTargetInfluences[index] = smoothValue;
91
+ // Handle blinking
92
+ let blinkValue = 0;
93
+ if (eyeBlink) {
94
+ if (currentTime >= nextBlinkTime.current && !isBlinking.current) {
95
+ isBlinking.current = true;
96
+ blinkStartTime.current = currentTime;
97
+ lastBlinkTime.current = currentTime;
98
+ nextBlinkTime.current =
99
+ currentTime +
100
+ Math.random() *
101
+ (BLINK_CONFIG.maxInterval - BLINK_CONFIG.minInterval) +
102
+ BLINK_CONFIG.minInterval;
103
+ }
104
+
105
+ if (isBlinking.current) {
106
+ const blinkProgress =
107
+ (currentTime - blinkStartTime.current) / BLINK_CONFIG.blinkDuration;
108
+ if (blinkProgress <= 0.5) {
109
+ // Eyes closing
110
+ blinkValue = blinkProgress * 2;
111
+ } else if (blinkProgress <= 1) {
112
+ // Eyes opening
113
+ blinkValue = 2 - blinkProgress * 2;
114
+ } else {
115
+ // Blink finished
116
+ isBlinking.current = false;
117
+ blinkValue = 0;
106
118
  }
107
- });
119
+ }
108
120
  }
109
- });
110
121
 
122
+ const currentViseme = updateCurrentViseme(currentTime / 1000);
123
+
124
+ // Update morph targets
125
+ Object.entries(headMeshRef.current.morphTargetDictionary).forEach(
126
+ ([key, index]) => {
127
+ if (typeof index === 'number') {
128
+ let targetValue = 0;
129
+
130
+ // Handle visemes (additive layer)
131
+ if (currentViseme && key === currentViseme.name) {
132
+ targetValue += currentViseme.weight * 1.3; // Amplify the effect
133
+ }
134
+
135
+ // Handle blinking (additive layer, only for 'eyesClosed')
136
+ if (key === 'eyesClosed' && eyeBlink) {
137
+ targetValue += blinkValue;
138
+ }
139
+
140
+ // Clamp the final value between 0 and 1
141
+ targetValue = MathUtils.clamp(targetValue, 0, 1);
142
+
143
+ // Apply smoothing
144
+ if (
145
+ headMeshRef.current &&
146
+ headMeshRef.current.morphTargetInfluences
147
+ ) {
148
+ headMeshRef.current.morphTargetInfluences[index] = MathUtils.lerp(
149
+ headMeshRef.current.morphTargetInfluences[index],
150
+ targetValue,
151
+ morphTargetSmoothing
152
+ );
153
+ }
154
+ }
155
+ }
156
+ );
157
+
158
+ // Update the animation mixer
159
+ mixer.current.update(0.01); // Fixed delta time for consistent animation speed
160
+ }
161
+ });
111
162
 
112
163
  return (
113
164
  <group position={AVATAR_POSITION}>
114
- {nodes.Hips && <primitive key="armature" object={nodes.Hips} />}
115
- {skinnedMeshes.map(
116
- (node: Object3D) =>
117
- node && (
118
- <primitive key={node.name} object={node} receiveShadow castShadow />
119
- )
120
- )}
165
+ <primitive object={scene} />
121
166
  </group>
122
167
  );
123
168
  }