diff --git a/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx b/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx index 01392a03..5fe82c56 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx +++ b/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx @@ -1,8 +1,15 @@ +// Import required dependencies import React, { useState, useEffect, useCallback } from 'react'; import AnimationControlPanel from './components/controls'; -import { FullbodyAvatar } from './components/FullbodyAvatar/fullbodyAvatar' +import { FullbodyAvatar } from './components/FullbodyAvatar/fullbodyAvatar'; import HalfBodyAvatar from './components/halfbodyAvatar'; +import { + BASE_ACTIONS, + MAPPING_BLEND_SHAPE_TO_EMOTION_CUSTOM_GLB, + MAPPING_BLEND_SHAPE_TO_EMOTION_RPM, +} from './constants'; +// Props interface for AvatarView component interface Props { showControls: boolean; animation?: string; @@ -18,43 +25,14 @@ interface Props { avatarDepth?: number; stopProcessing: () => void; resetVisemeQueue: () => void; - updateCurrentViseme: ( - currentTime: number - ) => { name: string; weight: number } | null; + updateCurrentViseme: (currentTime: number) => { name: string; weight: number } | null; setCameraZ: (value: number) => void; } -interface BaseAction { - weight: number; - action?: string; -} - -const baseActions: Record = { - Gioia1: { weight: 0 }, - Gioia2: { weight: 0 }, - Gioia3: { weight: 0 }, - Idle1: { weight: 1 }, - Idle2: { weight: 0 }, - Idle3: { weight: 0 }, - Idle4: { weight: 0 }, - Idle5: { weight: 0 }, - Rabbia1: { weight: 0 }, - Rabbia2: { weight: 0 }, - Rabbia3: { weight: 0 }, - Sorpresa1: { weight: 0 }, - Sorpresa2: { weight: 0 }, - Sorpresa3: { weight: 0 }, - Timore1: { weight: 0 }, - Timore2: { weight: 0 }, - Timore3: { weight: 0 }, - Tristezza1: { weight: 0 }, - Tristezza2: { weight: 0 }, - Tristezza3: { weight: 0 }, - Loading1: { weight: 0 }, - Loading2: { weight: 0 }, - Loading3: { weight: 0 }, -}; - +/** + * AvatarView Component + * Renders either a full body or half body 3D avatar with animations and morphing capabilities + */ export const AvatarView: React.FC = ({ stopProcessing, chatEmission, @@ -64,138 +42,137 @@ export const AvatarView: React.FC = ({ sex, eyeBlink, headMovement, - // speaking, halfBody, loading, - // isZoomed, - avatarHeight, - avatarDepth, + avatarHeight = 50, + avatarDepth = -50, updateCurrentViseme, resetVisemeQueue, setCameraZ, }) => { + // State management for avatar animations and morphing const [currentBaseAction, setCurrentBaseAction] = useState({ action: animation || 'Idle1', weight: 1, }); - - const [morphTargetInfluences, setMorphTargetInfluences] = useState<{ - [key: string]: number; - }>({}); - const [morphTargetDictionary, setMorphTargetDictionary] = useState<{ - [key: string]: number; - }>({}); - const [emotionMorphTargets, setEmotionMorphTargets] = useState<{ - [key: string]: number; - }>({}); - + const [morphTargetInfluences, setMorphTargetInfluences] = useState>({}); + const [morphTargetDictionary, setMorphTargetDictionary] = useState>({}); + const [emotionMorphTargets, setEmotionMorphTargets] = useState>({}); + const [isRPM, setIsRPM] = useState(false); const [timeScale, setTimeScale] = useState(0.8); - // Set the morph target influences for the given emotions - const setEmotionMorphTargetInfluences = useCallback((action: string) => { - if ( - action === 'Loading1' || - action === 'Loading2' || - action === 'Loading3' - ) { + // Map of basic emotions with their corresponding morph values + const emotionMap: Record> = { + Joy: { Joy: 1 }, + Anger: { Anger: 1 }, + Surprise: { Surprise: 1 }, + Sadness: { Sadness: 1 }, + Fear: { Fear: 1 }, + }; + + // Helper function to get default emotion state (all set to 0) + const getDefaultEmotions = () => + Object.keys(emotionMap).reduce((acc, key) => ({...acc, [key]: 0}), {}); + + // Handlers for different blend shape types + const handleRPMBlendShape = useCallback((outputContent: string) => + MAPPING_BLEND_SHAPE_TO_EMOTION_RPM[outputContent as keyof typeof MAPPING_BLEND_SHAPE_TO_EMOTION_RPM], + []); + + const handleCustomGLBBlendShape = useCallback((outputContent: string) => + MAPPING_BLEND_SHAPE_TO_EMOTION_CUSTOM_GLB[outputContent as keyof typeof MAPPING_BLEND_SHAPE_TO_EMOTION_CUSTOM_GLB], + []); + + // Handler for setting emotion morph target influences, used for RPM and GLB blend shapes + const setEmotionMorphTargetInfluences = useCallback((action: string, outputContent: string) => { + if (action.startsWith('Loading')) return; + + const defaultEmotions = getDefaultEmotions(); + + // If output content is default, set default emotions + if (outputContent === 'default') { + setEmotionMorphTargets(defaultEmotions); return; } - const emotionMap: Record> = { - Gioia: { Gioria: 1 }, - Rabbia: { Rabbia: 1 }, - Sorpresa: { Sorpresa: 1 }, - Tristezza: { Tristezza: 1 }, - Timore: { Timore: 1 }, - }; - - // Set all emotions to 0 - const defaultEmotions = Object.keys(emotionMap).reduce((acc, key) => { - acc[key] = 0; - return acc; - }, {} as Record); - - // Find the emotion that matches the action - const emotion = - Object.keys(emotionMap).find(key => action.startsWith(key)) || 'default'; - - // Set the emotion values - const emotionValues = - emotion === 'default' ? defaultEmotions : emotionMap[emotion]; - - setEmotionMorphTargets(_ => ({ - ...defaultEmotions, - ...emotionValues, - })); - }, []); + // If RPM, convert emotion to blend shape + /*from the chat output, we get the emotion and we convert it to the blend shapes + * we map the emotion to the blend shape, example: + * Anger -> {browDownLeft: 1, browDownRight: 0} + * Joy -> {browUpLeft: 1, browUpRight: 0} + * Surprise -> {browUpLeft: 1, browUpRight: 0} + * Sadness -> {browDownLeft: 1, browDownRight: 0} + * Fear -> {browDownLeft: 1, browDownRight: 0} + */ + if (isRPM) { + const emotion = handleRPMBlendShape(outputContent); + setEmotionMorphTargets((_) => ({...defaultEmotions, ...emotion})); + } else { + // If GLB, convert italian emotions to english ones + const emotion = handleCustomGLBBlendShape(outputContent); + const emotionValues = emotion === 'default' ? defaultEmotions : emotionMap[emotion]; + setEmotionMorphTargets((_) => ({...defaultEmotions, ...emotionValues})); + } + }, [isRPM, handleRPMBlendShape, handleCustomGLBBlendShape]); - const onBaseActionChange = useCallback((action: string) => { - setEmotionMorphTargetInfluences(action); - setCurrentBaseAction({ - action, - weight: 1, - }); - }, []); + // Callback handlers for various avatar state changes + const onBaseActionChange = useCallback((action: string, outputContent: string) => { - const onMorphTargetInfluencesChange = useCallback( - (influences: { [key: string]: number }) => { - setMorphTargetInfluences(prevInfluences => ({ - ...prevInfluences, - ...influences, - })); - }, - [] - ); + // Set emotion morph target influences + setEmotionMorphTargetInfluences(action, outputContent); - const onMorphTargetDictionaryChange = useCallback( - (dictionary: { [key: string]: number }) => { - setMorphTargetDictionary(dictionary); - }, - [] - ); + // Set current base action + setCurrentBaseAction({action, weight: 1}); + }, [setEmotionMorphTargetInfluences]); + + const onMorphTargetInfluencesChange = useCallback((influences: Record) => { + // Set morph target influences + setMorphTargetInfluences(prev => ({...prev, ...influences})); + }, []); - const modifyTimeScale = useCallback((value: number) => { - setTimeScale(value); + const onMorphTargetDictionaryChange = useCallback((dictionary: Record) => { + // Set morph target dictionary + setMorphTargetDictionary(dictionary); }, []); - // Set the emotion based on the chatEmission + // Effect to handle animation changes based on loading state and chat emissions useEffect(() => { - //Check if chatEmission has a tag - const hasOutputTag = chatEmission?.includes( - '' - ); + + // If loading, set a random loading animation + if (loading) { + const randomNumber = Math.floor(Math.random() * 3) + 1; + onBaseActionChange(`Loading${randomNumber}`, ''); + return; + } + + // If there's chat emission, set the corresponding emotion animation + const hasOutputTag = chatEmission?.includes(''); const outputContent = hasOutputTag - ? chatEmission - ?.split('')[1] - ?.split('')[0] - ?.trim() + ? chatEmission?.split('')[1]?.split('')[0]?.trim() : null; + // If there's an emotion, set the corresponding animation if (outputContent) { - //Based on the outputContent, set the emotion - //The outputContent could be: "Gioia", "Sorpresa", "Tristezza", "Rabbia", "Timore" - //Choose a random number between 1 and 3 const randomNumber = Math.floor(Math.random() * 3) + 1; - const emotion = `${outputContent}${randomNumber}`; - - onBaseActionChange(emotion); + onBaseActionChange(`${outputContent}${randomNumber}`, outputContent); } else { - //Set a random idle animation const randomNumber = Math.floor(Math.random() * 5) + 1; - const animation = `Idle${randomNumber === 3 ? 4 : randomNumber}`; - onBaseActionChange(animation); + onBaseActionChange(`Idle${randomNumber === 3 ? 4 : randomNumber}`, ''); } - }, [chatEmission]); - - useEffect(() => { - if (loading) { - //Choose a random number between 1 and 3 - const randomNumber = Math.floor(Math.random() * 3) + 1; - const animation = `Loading${randomNumber}`; - onBaseActionChange(animation); - } - }, [loading]); - + }, [chatEmission, loading, onBaseActionChange]); + + // Common props shared between full body and half body avatars + const commonAvatarProps = { + url, + onCameraZChange: setCameraZ, + setMorphTargetInfluences, + setMorphTargetDictionary, + updateCurrentViseme, + avatarHeight, + avatarDepth, + }; + + // Render avatar with controls return ( <> {showControls && ( @@ -205,40 +182,30 @@ export const AvatarView: React.FC = ({ onBaseActionChange={onBaseActionChange} onMorphTargetInfluencesChange={onMorphTargetInfluencesChange} onMorphTargetDictionaryChange={onMorphTargetDictionaryChange} - baseActions={baseActions} + baseActions={BASE_ACTIONS} currentBaseAction={currentBaseAction} - modifyTimeScale={modifyTimeScale} + modifyTimeScale={setTimeScale} /> )} + {halfBody ? ( ) : ( )} diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/AnimationController.ts b/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/AnimationController.ts index e7347f4b..2a341ddb 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/AnimationController.ts +++ b/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/AnimationController.ts @@ -1,6 +1,6 @@ import { AnimationState, AnimationConfig } from './types'; import { AnimationAction, AnimationMixer, LoopOnce } from 'three'; -import { DEFAULT_CONFIG } from '../constants'; +import { DEFAULT_CONFIG, MAX_IDLE_LOOPS_DEFAULT } from '../../constants'; /** * Controller class for managing avatar animations and transitions between states @@ -23,7 +23,7 @@ export class AnimationController { // Counter for number of times current idle has looped private currentIdleLoopCount: number = 0; // Maximum number of idle loops before forcing change - private readonly MAX_IDLE_LOOPS = 5; + private readonly MAX_IDLE_LOOPS = MAX_IDLE_LOOPS_DEFAULT; // Timestamp of last animation frame private lastAnimationTime: number = 0; // Flag to check if chat has already started @@ -60,9 +60,6 @@ export class AnimationController { // Force idle change after MAX_IDLE_LOOPS if (this.currentIdleLoopCount >= this.MAX_IDLE_LOOPS) { - // console.log( - // '[AnimationController] Max loops reached, changing idle animation' - // ); this.forceIdleChange(); } } @@ -74,7 +71,6 @@ export class AnimationController { * Forces transition to a new idle animation */ private forceIdleChange() { - // console.log('[AnimationController] Forcing idle change'); this.currentIdleLoopCount = 0; this.lastAnimationTime = 0; this.transitionTo(AnimationState.IDLE); @@ -133,6 +129,7 @@ export class AnimationController { try { let nextAction: AnimationAction | null = null; + // Select the next action based on the current state switch (state) { case AnimationState.LOADING: nextAction = this.actions[emotionName || 'Loading1']; diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx b/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx index 472c16e2..169d3433 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx +++ b/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx @@ -18,11 +18,12 @@ import { ANIMATION_URLS, DEFAULT_CONFIG, SCALE_LERP_FACTOR, -} from '../constants'; +} from '../../constants'; export function FullbodyAvatar({ url, sex, + setIsRpm, currentBaseAction, timeScale, eyeBlink, @@ -65,12 +66,15 @@ export function FullbodyAvatar({ ); if (headMesh) { + // console.log('[FullbodyAvatar] Head mesh found:', headMesh.name); morphTargetControllerRef.current = new MorphTargetController(headMesh); if (headMesh.morphTargetDictionary && headMesh.morphTargetInfluences) { + // console.log('[FullbodyAvatar] Setting morph target dictionary and influences', headMesh.morphTargetDictionary, headMesh.morphTargetInfluences); setMorphTargetDictionary(headMesh.morphTargetDictionary); const initialInfluences = Object.keys(headMesh.morphTargetDictionary) .reduce((acc, key) => ({ ...acc, [key]: 0 }), {}); + // console.log('[FullbodyAvatar] Setting initial influences', initialInfluences); setMorphTargetInfluences(initialInfluences); } } @@ -96,6 +100,12 @@ export function FullbodyAvatar({ object instanceof SkinnedMesh && (object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar') ) { + // console.log('[FullbodyAvatar] Found head mesh:', object.name); + if(object.name === 'GBNL__Head') { + setIsRpm(false); + } else { + setIsRpm(true); + } foundMesh = object; } }); diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/types.ts b/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/types.ts index 42bbfe6b..ed1545df 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/types.ts +++ b/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/types.ts @@ -14,6 +14,7 @@ export interface AnimationConfig { export interface FullbodyAvatarProps { url: string; sex: 'MALE' | 'FEMALE'; + setIsRpm: (value: boolean) => void; onLoaded?: () => void; currentBaseAction: { action: string; diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/MorphTargetController.ts b/src/components/Avatar/AvatarView/AvatarComponent/components/MorphTargetController.ts index 483c6547..89e37f3b 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/MorphTargetController.ts +++ b/src/components/Avatar/AvatarView/AvatarComponent/components/MorphTargetController.ts @@ -1,7 +1,10 @@ import { SkinnedMesh } from 'three'; import { MathUtils } from 'three'; -import { EMOTION_SMOOTHING, VISEME_SMOOTHING, BLINK_CONFIG } from './constants'; +import { EMOTION_SMOOTHING, VISEME_SMOOTHING, BLINK_CONFIG } from '../constants'; +/** + * Controller class for handling morph target animations including emotions, visemes and blinking + */ export class MorphTargetController { private headMesh: SkinnedMesh; private currentEmotionValues: Record = {}; @@ -10,7 +13,9 @@ export class MorphTargetController { constructor(headMesh: SkinnedMesh) { this.headMesh = headMesh; } - + /** + * Updates the morph target influences for emotions, visemes and blinking + */ updateMorphTargets( currentTime: number, emotionMorphTargets: Record, @@ -23,57 +28,64 @@ export class MorphTargetController { blinkStartTime: number; } ) { + // Validate required mesh properties exist if ( !this.headMesh.morphTargetDictionary || !this.headMesh.morphTargetInfluences ) { + console.error('[MorphTargetController] Missing morphTargetDictionary or morphTargetInfluences'); return; } + // Calculate blink value for this frame const blinkValue = this.calculateBlinkValue( currentTime, blinkState, eyeBlink ); + const currentEmotionKeys = new Set(Object.keys(emotionMorphTargets)); + // Process each morph target Object.entries(this.headMesh.morphTargetDictionary).forEach( ([key, index]) => { if (typeof index !== 'number') return; let targetValue = 0; - // Handle emotion morphs + // Handle emotion morphs with smoothing if (currentEmotionKeys.has(key)) { const targetEmotionValue = emotionMorphTargets[key]; const currentEmotionValue = this.currentEmotionValues[key] || 0; const newEmotionValue = MathUtils.lerp( currentEmotionValue, - targetEmotionValue * 2.5, + targetEmotionValue * 3, // Amplify emotion by 3x EMOTION_SMOOTHING ); + console.log(`[MorphTargetController] Emotion ${key}: current=${currentEmotionValue}, target=${targetEmotionValue}, new=${newEmotionValue}`); this.currentEmotionValues[key] = newEmotionValue; targetValue += newEmotionValue; } - // Handle viseme + // Add viseme influence if active if (currentViseme && key === currentViseme.name) { targetValue += currentViseme.weight; } - // Handle blinking + // Add blink influence if active if (key === 'eyesClosed' && eyeBlink) { targetValue += blinkValue; } - // Apply final value + // Clamp and smooth final value targetValue = MathUtils.clamp(targetValue, 0, 1); if (this.headMesh.morphTargetInfluences) { - this.headMesh.morphTargetInfluences[index] = MathUtils.lerp( + const finalValue = MathUtils.lerp( this.headMesh.morphTargetInfluences[index] || 0, targetValue, VISEME_SMOOTHING ); + this.headMesh.morphTargetInfluences[index] = finalValue; } } ); @@ -81,6 +93,9 @@ export class MorphTargetController { this.previousEmotionKeys = currentEmotionKeys; } + /** + * Calculates the blink value based on timing and state + */ private calculateBlinkValue( currentTime: number, blinkState: { @@ -95,6 +110,7 @@ export class MorphTargetController { let blinkValue = 0; + // Start new blink if it's time if (currentTime >= blinkState.nextBlinkTime && !blinkState.isBlinking) { blinkState.isBlinking = true; blinkState.blinkStartTime = currentTime; @@ -105,14 +121,21 @@ export class MorphTargetController { BLINK_CONFIG.minInterval; } + // Calculate blink animation progress if (blinkState.isBlinking) { const blinkProgress = (currentTime - blinkState.blinkStartTime) / BLINK_CONFIG.blinkDuration; + + // First half of blink - closing eyes if (blinkProgress <= 0.5) { blinkValue = blinkProgress * 2; - } else if (blinkProgress <= 1) { + } + // Second half of blink - opening eyes + else if (blinkProgress <= 1) { blinkValue = 2 - blinkProgress * 2; - } else { + } + // Blink complete + else { blinkState.isBlinking = false; blinkValue = 0; } diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/PositionController.ts b/src/components/Avatar/AvatarView/AvatarComponent/components/PositionController.ts index f05d6831..152022a8 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/PositionController.ts +++ b/src/components/Avatar/AvatarView/AvatarComponent/components/PositionController.ts @@ -1,5 +1,5 @@ import { Vector3, MathUtils } from 'three'; -import { AVATAR_POSITION, AVATAR_POSITION_ZOOMED } from './constants'; +import { AVATAR_POSITION, AVATAR_POSITION_ZOOMED } from '../constants'; export class AvatarPositionController { private currentScale: Vector3; diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/constants.ts b/src/components/Avatar/AvatarView/AvatarComponent/components/constants.ts deleted file mode 100644 index b37d0cd3..00000000 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/constants.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Vector3, Euler } from 'three'; -import { AnimationConfig } from './FullbodyAvatar/types'; - -export const AVATAR_POSITION = new Vector3(0, -1, 0); -export const AVATAR_ROTATION = new Euler(0.175, 0, 0); -export const AVATAR_POSITION_ZOOMED = new Vector3(0, -1.45, 0); -export const SCALE_LERP_FACTOR = 0.1; - -export const ANIMATION_URLS = { - MALE: 'https://assets.memori.ai/api/v2/asset/2c5e88a4-cf62-408b-9ef0-518b099dfcb2.glb', - FEMALE: - 'https://assets.memori.ai/api/v2/asset/2adc934b-24b2-45bd-94ad-ffec58d3cb32.glb', -}; - -export const BLINK_CONFIG = { - minInterval: 1000, - maxInterval: 5000, - blinkDuration: 150, -}; - -export const DEFAULT_CONFIG: AnimationConfig = { - fadeInDuration: 0.8, - fadeOutDuration: 0.8, - idleCount: 5, - timeScale: 1.0, -}; - -export const EMOTION_SMOOTHING = 0.3; -export const VISEME_SMOOTHING = 0.5; diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/controls.tsx b/src/components/Avatar/AvatarView/AvatarComponent/components/controls.tsx index ebd543c9..f759e84e 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/controls.tsx +++ b/src/components/Avatar/AvatarView/AvatarComponent/components/controls.tsx @@ -13,7 +13,7 @@ interface AdditiveAction { export interface AnimationControlPanelProps { baseActions: Record; - onBaseActionChange: (action: string) => void; + onBaseActionChange: (action: string, outputContent: string) => void; currentBaseAction: { action: string; weight: number; @@ -54,7 +54,7 @@ const AnimationControlPanel: React.FC = ({ baseNames.forEach(name => { const settings = baseActions[name]; panelSettingsRef.current[name] = () => { - onBaseActionChange(name); + onBaseActionChange(name, ''); }; const control = folder1.add(panelSettingsRef.current, name); diff --git a/src/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.tsx b/src/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.tsx index 5da2d42c..75359de1 100644 --- a/src/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.tsx +++ b/src/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.tsx @@ -9,7 +9,7 @@ import { AVATAR_POSITION, SCALE_LERP_FACTOR, AVATAR_POSITION_ZOOMED, -} from './constants'; +} from '../constants'; import { hideHands } from '../../utils/utils'; import useHeadMovement from '../../utils/useHeadMovement'; diff --git a/src/components/Avatar/AvatarView/AvatarComponent/constants.ts b/src/components/Avatar/AvatarView/AvatarComponent/constants.ts new file mode 100644 index 00000000..e14047bd --- /dev/null +++ b/src/components/Avatar/AvatarView/AvatarComponent/constants.ts @@ -0,0 +1,127 @@ +import { Vector3, Euler } from 'three'; +import { AnimationConfig } from './components/FullbodyAvatar/types'; + +// Position and rotation of the avatar +export const AVATAR_POSITION = new Vector3(0, -1, 0); +export const AVATAR_ROTATION = new Euler(0.175, 0, 0); +export const AVATAR_POSITION_ZOOMED = new Vector3(0, -1.45, 0); + +// Factor for lerping the scale of the avatar +export const SCALE_LERP_FACTOR = 0.1; + +// Maximum number of idle loops before forcing change +export const MAX_IDLE_LOOPS_DEFAULT = 5; + + +interface BaseAction { + weight: number; + action?: string; +} + +// Base animations for the avatar +export const BASE_ACTIONS: Record = { + Gioia1: { weight: 0 }, + Gioia2: { weight: 0 }, + Gioia3: { weight: 0 }, + Idle1: { weight: 1 }, + Idle2: { weight: 0 }, + Idle3: { weight: 0 }, + Idle4: { weight: 0 }, + Idle5: { weight: 0 }, + Rabbia1: { weight: 0 }, + Rabbia2: { weight: 0 }, + Rabbia3: { weight: 0 }, + Sorpresa1: { weight: 0 }, + Sorpresa2: { weight: 0 }, + Sorpresa3: { weight: 0 }, + Timore1: { weight: 0 }, + Timore2: { weight: 0 }, + Timore3: { weight: 0 }, + Tristezza1: { weight: 0 }, + Tristezza2: { weight: 0 }, + Tristezza3: { weight: 0 }, + Loading1: { weight: 0 }, + Loading2: { weight: 0 }, + Loading3: { weight: 0 }, +}; + +// Mapping of emotions from Italian to English +export const MAPPING_EMOTIONS_ITALIAN_TO_ENGLISH = { + Gioia: 'Joy', + Rabbia: 'Anger', + Sorpresa: 'Surprise', + Tristezza: 'Sadness', + Timore: 'Fear', +}; + +// Mapping of blend shapes to emotions +export const MAPPING_BLEND_SHAPE_TO_EMOTION_RPM = { + Rabbia: { + 'browDownLeft': 0.5, + 'browDownRight': 0.5, + 'browOuterUpLeft': 0.5, + 'browOuterUpRight': 0.5, + 'mouthSmile': -0.2, + }, + Timore: { + 'browOuterUpLeft': -0.5, + 'browOuterUpRight': -0.5, + 'eyeWideLeft': -0.5, + 'eyeWideRight': -0.5, + }, + Tristezza: { + 'browDownLeft': -0.5, + 'browDownRight': -0.5, + 'eyeSquintLeft': 0.5, + 'eyeSquintRight': 0.5, + 'mouthSmile': -0.6, + }, + Sorpresa: { + 'browInnerUp': 0.5, + 'browOuterUpLeft': 0.5, + 'browOuterUpRight': 0.5, + 'eyeWideLeft': 0.5, + 'eyeWideRight': 0.5, + }, + Gioia: { + 'browDownLeft': 0.5, + 'browDownRight': 0.5, + 'browInnerUp': 0.5, + 'mouthSmile': 0.8, + }, +}; + +// Mapping of blend shapes to emotions for custom GLB +export const MAPPING_BLEND_SHAPE_TO_EMOTION_CUSTOM_GLB = { + Gioia: 'Joy', + Rabbia: 'Anger', + Sorpresa: 'Surprise', + Tristezza: 'Sadness', + Timore: 'Fear', +}; + +// URL for the male avatar +export const ANIMATION_URLS = { + MALE: 'https://assets.memori.ai/api/v2/asset/2c5e88a4-cf62-408b-9ef0-518b099dfcb2.glb', + FEMALE: + 'https://assets.memori.ai/api/v2/asset/2adc934b-24b2-45bd-94ad-ffec58d3cb32.glb', +}; + +// Blink configuration +export const BLINK_CONFIG = { + minInterval: 1000, + maxInterval: 5000, + blinkDuration: 150, +}; + +// Default animation configuration +export const DEFAULT_CONFIG: AnimationConfig = { + fadeInDuration: 0.8, + fadeOutDuration: 0.8, + idleCount: 5, + timeScale: 1.0, +}; + +// Smoothing factors for emotion and viseme morphs +export const EMOTION_SMOOTHING = 0.3; +export const VISEME_SMOOTHING = 0.5; diff --git a/src/index.stories.tsx b/src/index.stories.tsx index 083e6a81..536c43ed 100644 --- a/src/index.stories.tsx +++ b/src/index.stories.tsx @@ -127,8 +127,8 @@ Giovanna.args = { layout: 'ZOOMED_FULL_BODY', }; -export const GiovannaProva = Template.bind({}); -GiovannaProva.args = { +export const GiovannaRPMProva = Template.bind({}); +GiovannaRPMProva.args = { memoriName: 'Giovanna Test', ownerUserName: 'andrea.patini3', memoriID: '431d9819-c958-442c-a799-f90617371c0c', @@ -140,10 +140,17 @@ GiovannaProva.args = { uiLang: 'EN', spokenLang: 'IT', layout: 'ZOOMED_FULL_BODY', - integrationID: 'e92ac275-39b5-474d-8f9e-826cc5284f1e', + integrationID: '061898c0-2138-49da-a6b3-5d86267aad05', initialQuestion: 'inizio simulazione', }; +export const GiovannaGLBProva = Template.bind({}); +GiovannaGLBProva.args = { + ...GiovannaRPMProva.args, + integrationID: 'd2099459-0f10-40cd-85e1-06e77a678723', + layout: 'ZOOMED_FULL_BODY', +}; + export const GiovannaProvaWithPreviousSession = Template.bind({}); GiovannaProvaWithPreviousSession.args = { memoriName: 'Giovanna Test',