@memori.ai/memori-react 7.5.1 → 7.6.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.
- package/CHANGELOG.md +44 -0
- package/README.md +10 -2
- package/dist/components/Avatar/Avatar.d.ts +2 -0
- package/dist/components/Avatar/Avatar.js +11 -6
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.d.ts +20 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +107 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/controls.d.ts +26 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/controls.js +59 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/controls.js.map +1 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.d.ts +30 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js +148 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js.map +1 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.d.ts +15 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js +77 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js.map +1 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/loader.d.ts +5 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/loader.js +12 -0
- package/dist/components/Avatar/AvatarView/AvatarComponent/components/loader.js.map +1 -0
- package/dist/components/Avatar/AvatarView/components/fullbodyAvatar.d.ts +2 -1
- package/dist/components/Avatar/AvatarView/components/fullbodyAvatar.js +3 -2
- package/dist/components/Avatar/AvatarView/components/fullbodyAvatar.js.map +1 -1
- package/dist/components/Avatar/AvatarView/index.d.ts +6 -1
- package/dist/components/Avatar/AvatarView/index.js +14 -84
- package/dist/components/Avatar/AvatarView/index.js.map +1 -1
- package/dist/components/Avatar/AvatarView/utils/useEyeBlink.d.ts +16 -2
- package/dist/components/Avatar/AvatarView/utils/useEyeBlink.js +62 -38
- package/dist/components/Avatar/AvatarView/utils/useEyeBlink.js.map +1 -1
- package/dist/components/Avatar/AvatarView/utils/useMouthAnimation.d.ts +16 -0
- package/dist/components/Avatar/AvatarView/utils/useMouthAnimation.js +59 -0
- package/dist/components/Avatar/AvatarView/utils/useMouthAnimation.js.map +1 -0
- package/dist/components/Avatar/AvatarView/utils/useSmile.js +1 -1
- package/dist/components/Avatar/AvatarView/utils/useSmile.js.map +1 -1
- package/dist/components/ChatBubble/ChatBubble.js +2 -3
- package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
- package/dist/components/CompletionProviderStatus/CompletionProviderStatus.d.ts +1 -1
- package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js +24 -3
- package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.d.ts +1 -1
- package/dist/components/MemoriWidget/MemoriWidget.js +25 -3
- package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/dist/components/StartPanel/StartPanel.js +1 -1
- package/dist/components/StartPanel/StartPanel.js.map +1 -1
- package/dist/components/layouts/HiddenChat.d.ts +4 -0
- package/dist/components/layouts/HiddenChat.js +51 -0
- package/dist/components/layouts/HiddenChat.js.map +1 -0
- package/dist/components/layouts/ZoomedFullBody.d.ts +4 -0
- package/dist/components/layouts/ZoomedFullBody.js +8 -0
- package/dist/components/layouts/ZoomedFullBody.js.map +1 -0
- package/dist/components/layouts/hidden-chat.css +184 -0
- package/dist/context/visemeContext.d.ts +27 -0
- package/dist/context/visemeContext.js +221 -0
- package/dist/context/visemeContext.js.map +1 -0
- package/dist/helpers/utils.d.ts +7 -0
- package/dist/helpers/utils.js +51 -1
- package/dist/helpers/utils.js.map +1 -1
- package/dist/index.js +20 -16
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -0
- package/esm/components/Avatar/Avatar.d.ts +2 -0
- package/esm/components/Avatar/Avatar.js +11 -6
- package/esm/components/Avatar/Avatar.js.map +1 -1
- package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.d.ts +20 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +102 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/controls.d.ts +26 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/controls.js +56 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/controls.js.map +1 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.d.ts +30 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js +145 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js.map +1 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.d.ts +15 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js +73 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js.map +1 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/loader.d.ts +5 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/loader.js +9 -0
- package/esm/components/Avatar/AvatarView/AvatarComponent/components/loader.js.map +1 -0
- package/esm/components/Avatar/AvatarView/components/fullbodyAvatar.d.ts +2 -1
- package/esm/components/Avatar/AvatarView/components/fullbodyAvatar.js +3 -2
- package/esm/components/Avatar/AvatarView/components/fullbodyAvatar.js.map +1 -1
- package/esm/components/Avatar/AvatarView/index.d.ts +6 -1
- package/esm/components/Avatar/AvatarView/index.js +15 -85
- package/esm/components/Avatar/AvatarView/index.js.map +1 -1
- package/esm/components/Avatar/AvatarView/utils/useEyeBlink.d.ts +16 -2
- package/esm/components/Avatar/AvatarView/utils/useEyeBlink.js +61 -38
- package/esm/components/Avatar/AvatarView/utils/useEyeBlink.js.map +1 -1
- package/esm/components/Avatar/AvatarView/utils/useMouthAnimation.d.ts +16 -0
- package/esm/components/Avatar/AvatarView/utils/useMouthAnimation.js +55 -0
- package/esm/components/Avatar/AvatarView/utils/useMouthAnimation.js.map +1 -0
- package/esm/components/Avatar/AvatarView/utils/useSmile.js +1 -1
- package/esm/components/Avatar/AvatarView/utils/useSmile.js.map +1 -1
- package/esm/components/ChatBubble/ChatBubble.js +2 -3
- package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
- package/esm/components/CompletionProviderStatus/CompletionProviderStatus.d.ts +1 -1
- package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js +24 -3
- package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.d.ts +1 -1
- package/esm/components/MemoriWidget/MemoriWidget.js +26 -4
- package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
- package/esm/components/StartPanel/StartPanel.js +1 -1
- package/esm/components/StartPanel/StartPanel.js.map +1 -1
- package/esm/components/layouts/HiddenChat.d.ts +4 -0
- package/esm/components/layouts/HiddenChat.js +48 -0
- package/esm/components/layouts/HiddenChat.js.map +1 -0
- package/esm/components/layouts/ZoomedFullBody.d.ts +4 -0
- package/esm/components/layouts/ZoomedFullBody.js +5 -0
- package/esm/components/layouts/ZoomedFullBody.js.map +1 -0
- package/esm/components/layouts/hidden-chat.css +184 -0
- package/esm/context/visemeContext.d.ts +27 -0
- package/esm/context/visemeContext.js +216 -0
- package/esm/context/visemeContext.js.map +1 -0
- package/esm/helpers/utils.d.ts +7 -0
- package/esm/helpers/utils.js +45 -0
- package/esm/helpers/utils.js.map +1 -1
- package/esm/index.js +20 -16
- package/esm/index.js.map +1 -1
- package/esm/styles.css +1 -0
- package/package.json +2 -2
- package/src/components/Avatar/Avatar.test.tsx +28 -20
- package/src/components/Avatar/Avatar.tsx +19 -5
- package/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx +222 -0
- package/src/components/Avatar/AvatarView/{components → AvatarComponent/components}/controls.tsx +16 -10
- package/src/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.tsx +234 -0
- package/src/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.tsx +123 -0
- package/src/components/Avatar/AvatarView/{components → AvatarComponent/components}/loader.tsx +1 -1
- package/src/components/Avatar/AvatarView/AvatarView.stories.tsx +47 -8
- package/src/components/Avatar/AvatarView/index.tsx +35 -167
- package/src/components/Avatar/AvatarView/utils/useEyeBlink.ts +89 -48
- package/src/components/Avatar/AvatarView/utils/useMouthAnimation.ts +93 -0
- package/src/components/Avatar/AvatarView/utils/useSmile.ts +1 -1
- package/src/components/ChatBubble/ChatBubble.tsx +3 -4
- package/src/components/CompletionProviderStatus/CompletionProviderStatus.tsx +33 -3
- package/src/components/CompletionProviderStatus/__snapshots__/CompletionProviderStatus.test.tsx.snap +18 -0
- package/src/components/MemoriWidget/MemoriWidget.tsx +60 -5
- package/src/components/StartPanel/StartPanel.tsx +1 -1
- package/src/components/layouts/Chat.test.tsx +7 -5
- package/src/components/layouts/FullPage.test.tsx +11 -8
- package/src/components/layouts/HiddenChat.test.tsx +37 -0
- package/src/components/layouts/HiddenChat.tsx +108 -0
- package/src/components/layouts/Totem.test.tsx +6 -4
- package/src/components/layouts/WebsiteAssistant.test.tsx +7 -5
- package/src/components/layouts/ZoomedFullBody.test.tsx +37 -0
- package/src/components/layouts/ZoomedFullBody.tsx +55 -0
- package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +210 -0
- package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +444 -0
- package/src/components/layouts/hidden-chat.css +184 -0
- package/src/components/layouts/layouts.stories.tsx +135 -19
- package/src/context/visemeContext.tsx +328 -0
- package/src/helpers/utils.ts +73 -0
- package/src/index.stories.tsx +40 -17
- package/src/index.tsx +82 -78
- package/src/styles.css +1 -0
- package/src/components/Avatar/AvatarView/components/fullbodyAvatar.tsx +0 -120
- package/src/components/Avatar/AvatarView/components/halfbodyAvatar.tsx +0 -69
- package/src/components/Avatar/AvatarView/utils/useMouthSpeaking.ts +0 -87
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Vector3,
|
|
4
|
+
Euler,
|
|
5
|
+
AnimationMixer,
|
|
6
|
+
SkinnedMesh,
|
|
7
|
+
Object3D,
|
|
8
|
+
AnimationAction,
|
|
9
|
+
} from 'three';
|
|
10
|
+
import { useAnimations, useGLTF } from '@react-three/drei';
|
|
11
|
+
import { useGraph, dispose, useFrame } from '@react-three/fiber';
|
|
12
|
+
import { correctMaterials, isSkinnedMesh } from '../../../../../helpers/utils';
|
|
13
|
+
import { useAvatarBlink } from '../../utils/useEyeBlink';
|
|
14
|
+
import { useViseme } from '../../../../../context/visemeContext';
|
|
15
|
+
|
|
16
|
+
const lerp = (start: number, end: number, alpha: number): number => {
|
|
17
|
+
return start * (1 - alpha) + end * alpha;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface FullbodyAvatarProps {
|
|
21
|
+
url: string;
|
|
22
|
+
sex: 'MALE' | 'FEMALE';
|
|
23
|
+
onLoaded?: () => void;
|
|
24
|
+
currentBaseAction: {
|
|
25
|
+
action: string;
|
|
26
|
+
weight: number;
|
|
27
|
+
};
|
|
28
|
+
timeScale: number;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
speaking?: boolean;
|
|
31
|
+
isZoomed?: boolean;
|
|
32
|
+
setMorphTargetInfluences: (influences: { [key: string]: number }) => void;
|
|
33
|
+
setMorphTargetDictionary: (dictionary: { [key: string]: number }) => void;
|
|
34
|
+
morphTargetInfluences: { [key: string]: number };
|
|
35
|
+
morphTargetDictionary: { [key: string]: number };
|
|
36
|
+
setMeshRef: any;
|
|
37
|
+
eyeBlink?: boolean;
|
|
38
|
+
clearVisemes: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const AVATAR_POSITION = new Vector3(0, -1, 0);
|
|
42
|
+
const AVATAR_ROTATION = new Euler(0.175, 0, 0);
|
|
43
|
+
const AVATAR_POSITION_ZOOMED = new Vector3(0, -1.45, 0);
|
|
44
|
+
|
|
45
|
+
const ANIMATION_URLS = {
|
|
46
|
+
MALE: 'https://assets.memori.ai/api/v2/asset/1c350a21-97d8-4add-82cc-9dc10767a26b.glb',
|
|
47
|
+
FEMALE:
|
|
48
|
+
'https://assets.memori.ai/api/v2/asset/c2b07166-de10-4c66-918b-7b7cd380cca7.glb',
|
|
49
|
+
};
|
|
50
|
+
const ANIMATION_DURATION = 3000; // Duration in milliseconds for non-idle animations
|
|
51
|
+
|
|
52
|
+
export default function FullbodyAvatar({
|
|
53
|
+
url,
|
|
54
|
+
sex,
|
|
55
|
+
onLoaded,
|
|
56
|
+
currentBaseAction,
|
|
57
|
+
timeScale,
|
|
58
|
+
isZoomed,
|
|
59
|
+
setMorphTargetInfluences,
|
|
60
|
+
setMorphTargetDictionary,
|
|
61
|
+
morphTargetInfluences,
|
|
62
|
+
eyeBlink,
|
|
63
|
+
setMeshRef,
|
|
64
|
+
clearVisemes,
|
|
65
|
+
}: FullbodyAvatarProps) {
|
|
66
|
+
const { scene } = useGLTF(url);
|
|
67
|
+
const { animations } = useGLTF(ANIMATION_URLS[sex]);
|
|
68
|
+
const { nodes, materials } = useGraph(scene);
|
|
69
|
+
const { actions } = useAnimations(animations, scene);
|
|
70
|
+
const [mixer] = useState(() => new AnimationMixer(scene));
|
|
71
|
+
|
|
72
|
+
const avatarMeshRef = useRef<SkinnedMesh>();
|
|
73
|
+
const currentActionRef = useRef<AnimationAction | null>(null);
|
|
74
|
+
const isTransitioningRef = useRef(false);
|
|
75
|
+
|
|
76
|
+
// Blink animation
|
|
77
|
+
useAvatarBlink({
|
|
78
|
+
enabled: eyeBlink || false,
|
|
79
|
+
setMorphTargetInfluences,
|
|
80
|
+
config: {
|
|
81
|
+
minInterval: 1500,
|
|
82
|
+
maxInterval: 4000,
|
|
83
|
+
blinkDuration: 120,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Idle animation when emotion animation is finished
|
|
88
|
+
const transitionToIdle = useCallback(() => {
|
|
89
|
+
if (!actions || isTransitioningRef.current) return;
|
|
90
|
+
|
|
91
|
+
isTransitioningRef.current = true;
|
|
92
|
+
|
|
93
|
+
const finishCurrentAnimation = () => {
|
|
94
|
+
if (currentActionRef.current && !currentActionRef.current.paused) {
|
|
95
|
+
const remainingTime = (currentActionRef.current.getClip().duration - currentActionRef.current.time) * 1000;
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
startIdleAnimation();
|
|
98
|
+
}, remainingTime);
|
|
99
|
+
} else {
|
|
100
|
+
startIdleAnimation();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const startIdleAnimation = () => {
|
|
105
|
+
const idleAnimations = Object.keys(actions).filter(key =>
|
|
106
|
+
key.startsWith('Idle')
|
|
107
|
+
);
|
|
108
|
+
const randomIdle =
|
|
109
|
+
idleAnimations[Math.floor(Math.random() * idleAnimations.length)];
|
|
110
|
+
|
|
111
|
+
const idleAction = actions[randomIdle];
|
|
112
|
+
const fadeOutDuration = 0.5;
|
|
113
|
+
const fadeInDuration = 0.5;
|
|
114
|
+
|
|
115
|
+
if (currentActionRef.current) {
|
|
116
|
+
currentActionRef.current.fadeOut(fadeOutDuration);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
idleAction?.reset().fadeIn(fadeInDuration).play();
|
|
120
|
+
currentActionRef.current = idleAction;
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
isTransitioningRef.current = false;
|
|
124
|
+
}, (fadeOutDuration + fadeInDuration) * 1000);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (currentActionRef.current && !currentActionRef.current.getClip().name.startsWith('Idle')) {
|
|
128
|
+
finishCurrentAnimation();
|
|
129
|
+
} else {
|
|
130
|
+
startIdleAnimation();
|
|
131
|
+
}
|
|
132
|
+
}, [actions]);
|
|
133
|
+
|
|
134
|
+
// Base animation
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!actions || !currentBaseAction.action || isTransitioningRef.current)
|
|
137
|
+
return;
|
|
138
|
+
|
|
139
|
+
const newAction = actions[currentBaseAction.action];
|
|
140
|
+
if (!newAction) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`Animation "${currentBaseAction.action}" not found in actions.`
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const fadeOutDuration = 0.8;
|
|
148
|
+
const fadeInDuration = 0.8;
|
|
149
|
+
|
|
150
|
+
if (!currentBaseAction.action.startsWith('Idle')) {
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
transitionToIdle();
|
|
153
|
+
}, ANIMATION_DURATION);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (currentActionRef.current) {
|
|
157
|
+
currentActionRef.current.fadeOut(fadeOutDuration);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
newAction.timeScale = timeScale;
|
|
161
|
+
newAction.reset().fadeIn(fadeInDuration).play();
|
|
162
|
+
currentActionRef.current = newAction;
|
|
163
|
+
}, [currentBaseAction, timeScale, actions, transitionToIdle]);
|
|
164
|
+
|
|
165
|
+
// Set up the mesh reference and morph target influences
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
correctMaterials(materials);
|
|
168
|
+
|
|
169
|
+
scene.traverse((object: Object3D) => {
|
|
170
|
+
if (
|
|
171
|
+
object instanceof SkinnedMesh &&
|
|
172
|
+
(object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar')
|
|
173
|
+
) {
|
|
174
|
+
avatarMeshRef.current = object;
|
|
175
|
+
setMeshRef(object);
|
|
176
|
+
|
|
177
|
+
if (object.morphTargetDictionary && object.morphTargetInfluences) {
|
|
178
|
+
setMorphTargetDictionary(object.morphTargetDictionary);
|
|
179
|
+
|
|
180
|
+
const initialInfluences = Object.keys(
|
|
181
|
+
object.morphTargetDictionary
|
|
182
|
+
).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
|
|
183
|
+
setMorphTargetInfluences(initialInfluences);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
onLoaded?.();
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
Object.values(materials).forEach(dispose);
|
|
192
|
+
Object.values(nodes).filter(isSkinnedMesh).forEach(dispose);
|
|
193
|
+
clearVisemes();
|
|
194
|
+
};
|
|
195
|
+
}, [
|
|
196
|
+
materials,
|
|
197
|
+
nodes,
|
|
198
|
+
url,
|
|
199
|
+
onLoaded,
|
|
200
|
+
setMorphTargetDictionary,
|
|
201
|
+
setMorphTargetInfluences,
|
|
202
|
+
setMeshRef,
|
|
203
|
+
clearVisemes,
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
// Update morph target influences
|
|
207
|
+
useFrame((_, delta) => {
|
|
208
|
+
if (avatarMeshRef.current && avatarMeshRef.current.morphTargetDictionary) {
|
|
209
|
+
updateMorphTargetInfluences();
|
|
210
|
+
}
|
|
211
|
+
mixer.update(delta * 0.001);
|
|
212
|
+
|
|
213
|
+
function updateMorphTargetInfluences() {
|
|
214
|
+
Object.entries(morphTargetInfluences).forEach(([key, value]) => {
|
|
215
|
+
const index = avatarMeshRef.current!.morphTargetDictionary![key];
|
|
216
|
+
if (typeof index === 'number' &&
|
|
217
|
+
avatarMeshRef.current!.morphTargetInfluences) {
|
|
218
|
+
const currentValue = avatarMeshRef.current!.morphTargetInfluences[index];
|
|
219
|
+
const smoothValue = lerp(currentValue, value, 0.1);
|
|
220
|
+
avatarMeshRef.current!.morphTargetInfluences[index] = smoothValue;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<group
|
|
228
|
+
position={isZoomed ? AVATAR_POSITION_ZOOMED : AVATAR_POSITION}
|
|
229
|
+
rotation={AVATAR_ROTATION}
|
|
230
|
+
>
|
|
231
|
+
<primitive object={scene} />
|
|
232
|
+
</group>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Object3D, SkinnedMesh, Vector3 } from 'three';
|
|
3
|
+
import { useGLTF } from '@react-three/drei';
|
|
4
|
+
import { correctMaterials, isSkinnedMesh } from '../../../../../helpers/utils';
|
|
5
|
+
import { useGraph, dispose, useFrame } from '@react-three/fiber';
|
|
6
|
+
import { useAvatarBlink } from '../../utils/useEyeBlink';
|
|
7
|
+
import useHeadMovement from '../../utils/useHeadMovement';
|
|
8
|
+
import { hideHands } from '../../utils/utils';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
interface HalfBodyAvatarProps {
|
|
12
|
+
url: string;
|
|
13
|
+
setMorphTargetInfluences: (morphTargetInfluences: any) => void;
|
|
14
|
+
headMovement?: boolean;
|
|
15
|
+
speaking?: boolean;
|
|
16
|
+
onLoaded?: () => void;
|
|
17
|
+
setMeshRef: (mesh: Object3D) => void;
|
|
18
|
+
clearVisemes: () => void;
|
|
19
|
+
setMorphTargetDictionary: (morphTargetDictionary: any) => void;
|
|
20
|
+
eyeBlink?: boolean;
|
|
21
|
+
morphTargetInfluences: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
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;
|
|
27
|
+
};
|
|
28
|
+
export default function HalfBodyAvatar({
|
|
29
|
+
url,
|
|
30
|
+
setMorphTargetInfluences,
|
|
31
|
+
setMorphTargetDictionary,
|
|
32
|
+
headMovement,
|
|
33
|
+
eyeBlink,
|
|
34
|
+
setMeshRef,
|
|
35
|
+
onLoaded,
|
|
36
|
+
clearVisemes,
|
|
37
|
+
morphTargetInfluences,
|
|
38
|
+
}: HalfBodyAvatarProps) {
|
|
39
|
+
const { scene } = useGLTF(url);
|
|
40
|
+
const { nodes, materials } = useGraph(scene);
|
|
41
|
+
const avatarMeshRef = useRef<SkinnedMesh | null>(null);
|
|
42
|
+
|
|
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);
|
|
53
|
+
|
|
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);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
onLoaded?.();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
setupAvatar();
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
const disposeObjects = () => {
|
|
78
|
+
Object.values(materials).forEach(dispose);
|
|
79
|
+
Object.values(nodes).filter(isSkinnedMesh).forEach(dispose);
|
|
80
|
+
clearVisemes();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
disposeObjects();
|
|
84
|
+
};
|
|
85
|
+
}, [materials, nodes, url, onLoaded, clearVisemes]);
|
|
86
|
+
|
|
87
|
+
const skinnedMeshes = useMemo(
|
|
88
|
+
() => Object.values(nodes).filter(isSkinnedMesh),
|
|
89
|
+
[nodes]
|
|
90
|
+
);
|
|
91
|
+
|
|
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;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<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
|
+
)}
|
|
121
|
+
</group>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { Meta, Story } from '@storybook/react';
|
|
3
3
|
import I18nWrapper from '../../../I18nWrapper';
|
|
4
4
|
import AvatarView, { Props } from './index';
|
|
5
|
+
import { VisemeProvider } from '../../../context/visemeContext';
|
|
5
6
|
|
|
6
7
|
const meta: Meta = {
|
|
7
8
|
title: 'RPM 3D Avatar',
|
|
@@ -60,9 +61,7 @@ const meta: Meta = {
|
|
|
60
61
|
},
|
|
61
62
|
},
|
|
62
63
|
parameters: {
|
|
63
|
-
controls: { expanded: false,
|
|
64
|
-
},
|
|
65
|
-
|
|
64
|
+
controls: { expanded: false },
|
|
66
65
|
},
|
|
67
66
|
};
|
|
68
67
|
|
|
@@ -76,11 +75,13 @@ const Template: Story<Props> = args => {
|
|
|
76
75
|
|
|
77
76
|
return hydrated ? (
|
|
78
77
|
<I18nWrapper>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
<VisemeProvider>
|
|
79
|
+
<AvatarView
|
|
80
|
+
{...args}
|
|
81
|
+
url={args.url + `#${new Date(Date.now()).toISOString()}`}
|
|
82
|
+
key={Date.now()}
|
|
83
|
+
/>
|
|
84
|
+
</VisemeProvider>
|
|
84
85
|
</I18nWrapper>
|
|
85
86
|
) : (
|
|
86
87
|
<></>
|
|
@@ -95,6 +96,8 @@ Default.args = {
|
|
|
95
96
|
headMovement: false,
|
|
96
97
|
rotateAvatar: false,
|
|
97
98
|
speaking: false,
|
|
99
|
+
clearVisemes: () => {},
|
|
100
|
+
setMeshRef: () => {},
|
|
98
101
|
url: 'https://assets.memori.ai/api/v2/asset/b791f77c-1a94-4272-829e-eca82fcc62b7.glb',
|
|
99
102
|
fallbackImg:
|
|
100
103
|
'https://assets.memori.ai/api/v2/asset/d8035229-08cf-42a7-a532-ab051df2603d.png',
|
|
@@ -102,6 +105,8 @@ Default.args = {
|
|
|
102
105
|
|
|
103
106
|
export const EyeBlink = Template.bind({});
|
|
104
107
|
EyeBlink.args = {
|
|
108
|
+
clearVisemes: () => {},
|
|
109
|
+
setMeshRef: () => {},
|
|
105
110
|
eyeBlink: true,
|
|
106
111
|
headMovement: false,
|
|
107
112
|
rotateAvatar: false,
|
|
@@ -113,6 +118,8 @@ EyeBlink.args = {
|
|
|
113
118
|
|
|
114
119
|
export const HeadMovement = Template.bind({});
|
|
115
120
|
HeadMovement.args = {
|
|
121
|
+
clearVisemes: () => {},
|
|
122
|
+
setMeshRef: () => {},
|
|
116
123
|
eyeBlink: false,
|
|
117
124
|
headMovement: true,
|
|
118
125
|
rotateAvatar: false,
|
|
@@ -124,6 +131,8 @@ HeadMovement.args = {
|
|
|
124
131
|
|
|
125
132
|
export const RotateAvatar = Template.bind({});
|
|
126
133
|
RotateAvatar.args = {
|
|
134
|
+
clearVisemes: () => {},
|
|
135
|
+
setMeshRef: () => {},
|
|
127
136
|
eyeBlink: false,
|
|
128
137
|
headMovement: false,
|
|
129
138
|
rotateAvatar: true,
|
|
@@ -135,6 +144,8 @@ RotateAvatar.args = {
|
|
|
135
144
|
|
|
136
145
|
export const Speaking = Template.bind({});
|
|
137
146
|
Speaking.args = {
|
|
147
|
+
clearVisemes: () => {},
|
|
148
|
+
setMeshRef: () => {},
|
|
138
149
|
eyeBlink: false,
|
|
139
150
|
headMovement: false,
|
|
140
151
|
rotateAvatar: false,
|
|
@@ -151,12 +162,30 @@ Fullbody.args = {
|
|
|
151
162
|
headMovement: true,
|
|
152
163
|
rotateAvatar: true,
|
|
153
164
|
speaking: false,
|
|
165
|
+
clearVisemes: () => {},
|
|
166
|
+
setMeshRef: () => {},
|
|
154
167
|
url: 'https://models.readyplayer.me/63b55751f17e295642bf07a2.glb',
|
|
155
168
|
fallbackImg:
|
|
156
169
|
'https://assets.memori.ai/api/v2/asset/3049582f-db5f-452c-913d-e4340d4afd0a.png',
|
|
157
170
|
halfBody: false,
|
|
158
171
|
};
|
|
159
172
|
|
|
173
|
+
export const FullbodyZoomed = Template.bind({});
|
|
174
|
+
FullbodyZoomed.args = {
|
|
175
|
+
sex: 'FEMALE',
|
|
176
|
+
eyeBlink: true,
|
|
177
|
+
headMovement: true,
|
|
178
|
+
rotateAvatar: true,
|
|
179
|
+
speaking: false,
|
|
180
|
+
isZoomed: true,
|
|
181
|
+
clearVisemes: () => {},
|
|
182
|
+
setMeshRef: () => {},
|
|
183
|
+
url: 'https://assets.memori.ai/api/v2/asset/3f5ef41c-6c4c-449c-888d-cf9c89782528.glb',
|
|
184
|
+
fallbackImg:
|
|
185
|
+
'https://assets.memori.ai/api/v2/asset/3049582f-db5f-452c-913d-e4340d4afd0a.png',
|
|
186
|
+
halfBody: false,
|
|
187
|
+
};
|
|
188
|
+
|
|
160
189
|
export const FullbodyAnimatedIdle = Template.bind({});
|
|
161
190
|
FullbodyAnimatedIdle.args = {
|
|
162
191
|
sex: 'MALE',
|
|
@@ -164,6 +193,8 @@ FullbodyAnimatedIdle.args = {
|
|
|
164
193
|
headMovement: true,
|
|
165
194
|
rotateAvatar: true,
|
|
166
195
|
speaking: false,
|
|
196
|
+
clearVisemes: () => {},
|
|
197
|
+
setMeshRef: () => {},
|
|
167
198
|
url: 'https://models.readyplayer.me/63b55751f17e295642bf07a2.glb',
|
|
168
199
|
fallbackImg:
|
|
169
200
|
'https://assets.memori.ai/api/v2/asset/3049582f-db5f-452c-913d-e4340d4afd0a.png',
|
|
@@ -178,6 +209,8 @@ FullbodyAnimatedLoading.args = {
|
|
|
178
209
|
headMovement: true,
|
|
179
210
|
rotateAvatar: true,
|
|
180
211
|
speaking: false,
|
|
212
|
+
clearVisemes: () => {},
|
|
213
|
+
setMeshRef: () => {},
|
|
181
214
|
url: 'https://models.readyplayer.me/63b55751f17e295642bf07a2.glb',
|
|
182
215
|
fallbackImg:
|
|
183
216
|
'https://assets.memori.ai/api/v2/asset/3049582f-db5f-452c-913d-e4340d4afd0a.png',
|
|
@@ -192,6 +225,8 @@ FullbodyAnimatedSpeaking.args = {
|
|
|
192
225
|
headMovement: true,
|
|
193
226
|
rotateAvatar: true,
|
|
194
227
|
speaking: true,
|
|
228
|
+
clearVisemes: () => {},
|
|
229
|
+
setMeshRef: () => {},
|
|
195
230
|
url: 'https://models.readyplayer.me/63b55751f17e295642bf07a2.glb',
|
|
196
231
|
fallbackImg:
|
|
197
232
|
'https://assets.memori.ai/api/v2/asset/3049582f-db5f-452c-913d-e4340d4afd0a.png',
|
|
@@ -206,6 +241,8 @@ FullbodyFemale.args = {
|
|
|
206
241
|
headMovement: true,
|
|
207
242
|
rotateAvatar: true,
|
|
208
243
|
speaking: false,
|
|
244
|
+
clearVisemes: () => {},
|
|
245
|
+
setMeshRef: () => {},
|
|
209
246
|
url: 'https://models.readyplayer.me/650d50c2663b19e0d2831b2b.glb',
|
|
210
247
|
fallbackImg:
|
|
211
248
|
'https://assets.memori.ai/api/v2/asset/3049582f-db5f-452c-913d-e4340d4afd0a.png',
|
|
@@ -219,6 +256,8 @@ FullbodyAnimatedFemale.args = {
|
|
|
219
256
|
headMovement: true,
|
|
220
257
|
rotateAvatar: true,
|
|
221
258
|
speaking: true,
|
|
259
|
+
clearVisemes: () => {},
|
|
260
|
+
setMeshRef: () => {},
|
|
222
261
|
url: 'https://models.readyplayer.me/650d50c2663b19e0d2831b2b.glb',
|
|
223
262
|
fallbackImg:
|
|
224
263
|
'https://assets.memori.ai/api/v2/asset/3049582f-db5f-452c-913d-e4340d4afd0a.png',
|