Skip to content

Commit

Permalink
fix(animation): Act 992 nova animation transition fix
Browse files Browse the repository at this point in the history
* fix(animation): ACT-992 use current anim time - nova

* fix(animation): ACT-992 use animation service loader

* feat(animation): ACT-992 animation configuration
  • Loading branch information
Jarvv authored Sep 26, 2024
1 parent 6fec522 commit 37a4846
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 39 deletions.
21 changes: 18 additions & 3 deletions src/App/components/Nova.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@ import { SettingsPanel } from './SettingsPanel';

const idleUrl = 'https://readyplayerme-assets.s3.amazonaws.com/animations/nova-male-idle.glb';
const victoryUrl = 'https://readyplayerme-assets.s3.amazonaws.com/animations/nova-victory-03.glb';
const modelUrl =
'https://api.readyplayer.dev/v3/avatars/66e2cecfbd5d3e60f8cdbde5.glb?meshCompression=true&textureQuality=medium&meshSimplify=0&morphTargetsGroup=Editor+combined';

const animations: Record<string, string> = {
idle: idleUrl,
victory: victoryUrl
};

const modelOneUrl =
'https://api.readyplayer.dev/v3/avatars/66e02a5804466d05776a8f80.glb?meshCompression=true&textureQuality=medium&meshLOD=0&morphTargetsGroup=Basic expressions';
const modelTwoUrl =
'https://api.readyplayer.dev/v3/avatars/66e00aca4ec56c8f76e5da75.glb?meshCompression=true&textureQuality=medium&meshLOD=0&morphTargetsGroup=Basic expressions';

const models: Record<string, string> = {
one: modelOneUrl,
two: modelTwoUrl
};

export const AvatarNova: React.FC = () => {
const [activeAnimation, setActiveAnimation] = React.useState<string>('idle');
const [modelSrc, setModelSrc] = React.useState<string>(models.one);

return (
<>
Expand All @@ -27,9 +36,15 @@ export const AvatarNova: React.FC = () => {
<button type="button" onClick={() => setActiveAnimation('victory')}>
Set victory animation
</button>
<button type="button" onClick={() => setModelSrc(models.one)}>
Set Model1
</button>
<button type="button" onClick={() => setModelSrc(models.two)}>
Set Model2
</button>
</SettingsPanel>
<Avatar
modelSrc={modelUrl}
modelSrc={modelSrc}
emotion={emotions.smile}
animations={animations}
activeAnimation={activeAnimation}
Expand Down
8 changes: 8 additions & 0 deletions src/components/Avatar/Avatar.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SpawnState,
EffectConfiguration,
LightingProps,
AnimationConfiguration,
MaterialConfiguration
} from 'src/types';
import { BaseCanvas } from 'src/components/BaseCanvas';
Expand Down Expand Up @@ -155,6 +156,10 @@ export interface AvatarProps extends LightingProps, EnvironmentProps, Omit<BaseM
children?: ReactNode;
animations?: Record<string, string>;
activeAnimation?: string;
/**
* Control properties of animations.
*/
animationConfig?: AnimationConfiguration;
/**
* Control properties of materials.
*/
Expand Down Expand Up @@ -203,6 +208,7 @@ const Avatar: FC<AvatarProps> = ({
backLightPosition,
lightTarget,
fov = 50,
animationConfig,
materialConfig
}) => {
const setSpawnState = useSetAtom(spawnState);
Expand All @@ -226,6 +232,7 @@ const Avatar: FC<AvatarProps> = ({
scale={scale}
onLoaded={onLoaded}
bloom={effects?.bloom}
animationConfig={animationConfig}
materialConfig={materialConfig}
/>
);
Expand Down Expand Up @@ -299,6 +306,7 @@ const Avatar: FC<AvatarProps> = ({
effects?.bloom,
idleRotation,
headMovement,
animationConfig,
materialConfig
]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,97 +1,108 @@
import React, { FC, useEffect, useRef, useState } from 'react';
import { useFrame, useGraph } from '@react-three/fiber';
import { AnimationAction, AnimationClip, AnimationMixer, Group } from 'three';
import { GLTFLoader } from 'three-stdlib';
import { AnimationAction, AnimationClip, AnimationMixer } from 'three';

import { Model } from 'src/components/Models/Model';
import { BaseModelProps } from 'src/types';
import { useEmotion, useGltfCachedLoader, useIdleExpression } from 'src/services';
import { AnimationConfiguration, BaseModelProps } from 'src/types';
import { useEmotion, useFallback, useGltfCachedLoader, useIdleExpression } from 'src/services';
import { Emotion } from 'src/components/Avatar/Avatar.component';
import { loadAnimationClip } from 'src/services/Animation.service';

export interface MultipleAnimationModelProps extends BaseModelProps {
modelSrc: string | Blob;
animations: Record<string, string>;
activeAnimation: string;
scale?: number;
emotion?: Emotion;
animationConfig?: AnimationConfiguration;
}

export const MultipleAnimationModel: FC<MultipleAnimationModelProps> = ({
modelSrc,
animations,
activeAnimation,
scale = 1,
setModelFallback,
onLoaded,
emotion,
bloom,
animationConfig,
materialConfig
}) => {
const groupRef = useRef<Group>(null);
const mixerRef = useRef<AnimationMixer | null>(null);
const activeActionRef = useRef<AnimationAction | null>(null);
const animationTimeRef = useRef<number>(0);

const [loadedAnimations, setLoadedAnimations] = useState<Record<string, AnimationClip>>({});
const [activeAction, setActiveAction] = useState<AnimationAction | null>(null);

const { scene } = useGltfCachedLoader(modelSrc);
const { nodes } = useGraph(scene);

useEffect(() => {
if (scene && groupRef.current) {
groupRef.current.add(scene);
const loadAllAnimations = async () => {
const clips: Record<string, AnimationClip> = {};

await Promise.all(
Object.keys(animations).map(async (name) => {
const newClip = await loadAnimationClip(animations[name]);
clips[name] = newClip;
})
);

setLoadedAnimations(clips);
};

mixerRef.current = new AnimationMixer(scene);
loadAllAnimations();
}, [animations]);

useEffect(() => {
if (scene) {
const mixer = new AnimationMixer(scene);
mixerRef.current = mixer;

if (activeActionRef.current) {
const newAction = mixer.clipAction(activeActionRef.current.getClip());

newAction.play();
mixer.update(animationTimeRef.current);

activeActionRef.current = newAction;
}
}

return () => {
mixerRef.current?.stopAllAction();
mixerRef.current?.uncacheRoot(scene);
};
}, [scene]);

useEffect(() => {
const loader = new GLTFLoader();
Object.keys(animations).forEach((name) => {
loader.load(animations[name], (gltf) => {
const newClip = gltf.animations[0];
setLoadedAnimations((prev) => ({
...prev,
[name]: newClip
}));
});
});
}, [animations]);

useEffect(() => {
const mixer = mixerRef.current;
const prevAction = activeActionRef.current;
const newClip = loadedAnimations[activeAnimation];

if (!newClip || !mixer) return;
if (prevAction && prevAction.getClip().name === newClip.name) return;

const newAction = mixer.clipAction(newClip);
const prevAction = activeAction;
activeActionRef.current = newAction;

if (prevAction) {
newAction.reset();
newAction.crossFadeFrom(prevAction, 0.5, true);
newAction.crossFadeFrom(prevAction, animationConfig?.crossfadeDuration ?? 0.5, true);
}

newAction.play();
setActiveAction(newAction);
}, [activeAnimation, loadedAnimations, activeAction, nodes.Armature]);
}, [activeAnimation, loadedAnimations, animationConfig]);

useFrame((state, delta) => {
mixerRef.current?.update(delta);
animationTimeRef.current = activeActionRef.current?.time || 0;
});

useEmotion(nodes, emotion);
useIdleExpression('blink', nodes);
useFallback(nodes, setModelFallback);

return (
<Model
modelRef={groupRef}
scene={scene}
scale={scale}
onLoaded={onLoaded}
bloom={bloom}
materialConfig={materialConfig}
/>
);
return <Model scene={scene} scale={scale} onLoaded={onLoaded} bloom={bloom} materialConfig={materialConfig} />;
};
1 change: 1 addition & 0 deletions src/components/Models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { FloatingModel } from './FloatingModel';
export { HalfBodyModel } from './HalfBodyModel';
export { StaticModel } from './StaticModel';
export { PoseModel } from './PoseModel';
export { MultipleAnimationModel } from './MultipleAnimationModel';
export { Model } from './Model';
export { EnvironmentModel } from './EnvironmentModel';
7 changes: 7 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ export type EffectConfiguration = {
vignette?: boolean;
};

export type AnimationConfiguration = {
/**
* Duration of the crossfade between animations.
*/
crossfadeDuration?: number;
};

export interface SpawnState {
/**
* Add a custom loaded effect like particles when avatar is loaded, animate them with a custom animation.
Expand Down

0 comments on commit 37a4846

Please sign in to comment.