Skip to content

Commit

Permalink
Merge pull request #92 from readyplayerme/feature/smooth-animations
Browse files Browse the repository at this point in the history
Feature/smooth animations
  • Loading branch information
dan-rpm authored Sep 23, 2024
2 parents d8cc24b + 209f83d commit d40a999
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 105 deletions.
81 changes: 16 additions & 65 deletions src/App/App.component.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,11 @@
import React from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AvatarDevelop } from './components/Develop';
import { AvatarTest } from './components/Test';
import { AvatarNova } from './components/Nova';

import { Avatar, CAMERA } from 'src/components/Avatar';

import { Sparkles, StatsGl } from '@react-three/drei';
import { EnvironmentModel } from 'src/components/Models';
import styles from './App.module.scss';

const AvatarDevelop: React.FC = () => (
<div className={styles.app}>
<div className={styles.settings}>
<div className={styles.wrapper}>
<h3 className={styles.title}>localhost playground</h3>
<div className={styles.content}>
Drop your content in there to test the avatar props without shrinking the canvas
</div>
</div>
</div>
<div className={styles.container}>
<div className={styles.card}>
<Avatar
modelSrc="https://models.readyplayer.me/64d61e9e17883fd73ebe5eb7.glb?morphTargets=ARKit,Eyes Extra&textureAtlas=none&lod=0"
shadows
animationSrc="/male-idle-2.fbx"
style={{ background: 'rgb(9,20,26)' }}
fov={45}
effects={{
ambientOcclusion: true
}}
>
<StatsGl />
<EnvironmentModel environment="spaceStation" scale={1} />
<Sparkles count={70} scale={3} size={3} speed={1} opacity={0.04} color="#ccff00" />
</Avatar>
</div>
</div>
</div>
);

const AvatarTest: React.FC = () => {
const urlParams = new URLSearchParams(window.location.search);
const modelUrl = urlParams.get('modelUrl')
? decodeURIComponent(urlParams.get('modelUrl') || '')
: 'https://models.readyplayer.me/64d61e9e17883fd73ebe5eb7.glb?morphTargets=ARKit,Eyes Extra&textureAtlas=none&lod=0';
const zoomLevel = urlParams.get('zoomLevel')
? parseFloat(urlParams.get('zoomLevel') || '1')
: CAMERA.CONTROLS.FULL_BODY.MAX_DISTANCE;

return (
<div className={styles.app}>
<div className={styles.container}>
<div className={styles.card}>
<Avatar
modelSrc={modelUrl}
shadows
style={{ background: 'rgb(9,20,26)' }}
fov={45}
cameraInitialDistance={zoomLevel}
effects={{
ambientOcclusion: true
}}
/>
</div>
</div>
</div>
);
};

const router = createBrowserRouter([
{
path: '/',
Expand All @@ -75,9 +14,21 @@ const router = createBrowserRouter([
{
path: '/test',
element: <AvatarTest />
},
{
path: '/nova',
element: <AvatarNova />
}
]);

const App: React.FC = () => <RouterProvider router={router} />;
const App: React.FC = () => (
<div className={styles.app}>
<div className={styles.container}>
<div className={styles.card}>
<RouterProvider router={router} />
</div>
</div>
</div>
);

export default App;
38 changes: 0 additions & 38 deletions src/App/App.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,6 @@ body,
width: 100%;
}

.settings {
z-index: 2;
position: fixed;
top: 72px;
right: 24px;
width: 100%;
max-width: 320px;
background-color: rgba(243, 243, 243, 0.9);
border: 1px solid #b9b9b9;
border-radius: 4px;
overflow: hidden;
}

.wrapper {
position: relative;
height: 100%;
width: 100%;
}

.title {
font-size: 18px;
font-weight: 600;
font-family: Arial, sans-serif;
color: black;
display: block;
padding: 8px;
margin: 0;
border-bottom: 1px solid #b9b9b9;
}

.content {
position: relative;
padding: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}

.stats {
left: 80px !important;
}
27 changes: 27 additions & 0 deletions src/App/components/Develop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { Sparkles, StatsGl } from '@react-three/drei';

import { Avatar } from 'src/components/Avatar';
import { EnvironmentModel } from 'src/components/Models';

import { SettingsPanel } from './SettingsPanel';

export const AvatarDevelop: React.FC = () => (
<>
<SettingsPanel />
<Avatar
modelSrc="https://models.readyplayer.me/64d61e9e17883fd73ebe5eb7.glb?morphTargets=ARKit,Eyes Extra&textureAtlas=none&lod=0"
shadows
animationSrc="/male-idle-2.fbx"
style={{ background: 'rgb(9,20,26)' }}
fov={45}
effects={{
ambientOcclusion: true
}}
>
<StatsGl />
<EnvironmentModel environment="spaceStation" scale={1} />
<Sparkles count={70} scale={3} size={3} speed={1} opacity={0.04} color="#ccff00" />
</Avatar>
</>
);
45 changes: 45 additions & 0 deletions src/App/components/Nova.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { StatsGl } from '@react-three/drei';

import { Avatar, CAMERA } from 'src/components/Avatar';
import { emotions } from 'src/services/Stories.service';
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
};

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

return (
<>
<SettingsPanel>
<button type="button" onClick={() => setActiveAnimation('idle')}>
Set idle animation
</button>
<button type="button" onClick={() => setActiveAnimation('victory')}>
Set victory animation
</button>
</SettingsPanel>
<Avatar
modelSrc={modelUrl}
emotion={emotions.smile}
animations={animations}
activeAnimation={activeAnimation}
shadows
style={{ background: 'rgb(9,20,26)' }}
fov={45}
cameraInitialDistance={CAMERA.CONTROLS.FULL_BODY.MAX_DISTANCE}
>
<StatsGl />
</Avatar>
</>
);
};
37 changes: 37 additions & 0 deletions src/App/components/SettingsPanel.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.settings {
z-index: 2;
position: fixed;
top: 72px;
right: 24px;
width: 100%;
max-width: 320px;
background-color: rgba(243, 243, 243, 0.9);
border: 1px solid #b9b9b9;
border-radius: 4px;
overflow: hidden;
}

.wrapper {
position: relative;
height: 100%;
width: 100%;
}

.title {
font-size: 18px;
font-weight: 600;
font-family: Arial, sans-serif;
color: black;
display: block;
padding: 8px;
margin: 0;
border-bottom: 1px solid #b9b9b9;
}

.content {
position: relative;
padding: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
14 changes: 14 additions & 0 deletions src/App/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

import styles from './SettingsPanel.module.scss';

export const SettingsPanel: React.FC<React.PropsWithChildren> = ({
children = 'Drop your content in there to test the avatar props without shrinking the canvas'
}) => (
<div className={styles.settings}>
<div className={styles.wrapper}>
<h3 className={styles.title}>localhost playground</h3>
<div className={styles.content}>{children}</div>
</div>
</div>
);
26 changes: 26 additions & 0 deletions src/App/components/Test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

import { Avatar, CAMERA } from 'src/components/Avatar';

export const AvatarTest: React.FC = () => {
const urlParams = new URLSearchParams(window.location.search);
const modelUrl = urlParams.get('modelUrl')
? decodeURIComponent(urlParams.get('modelUrl') || '')
: 'https://models.readyplayer.me/64d61e9e17883fd73ebe5eb7.glb?morphTargets=ARKit,Eyes Extra&textureAtlas=none&lod=0';
const zoomLevel = urlParams.get('zoomLevel')
? parseFloat(urlParams.get('zoomLevel') || '1')
: CAMERA.CONTROLS.FULL_BODY.MAX_DISTANCE;

return (
<Avatar
modelSrc={modelUrl}
shadows
style={{ background: 'rgb(9,20,26)' }}
fov={45}
cameraInitialDistance={zoomLevel}
effects={{
ambientOcclusion: true
}}
/>
);
};
36 changes: 34 additions & 2 deletions src/components/Avatar/Avatar.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import Loader from 'src/components/Loader';
import Bloom from 'src/components/Bloom/Bloom.component';
import { BlendFunction } from 'postprocessing';
import Lights from 'src/components/Lights/Lights.component';
import { spawnState } from '../../state/spawnAtom';
import { MultipleAnimationModel } from 'src/components/Models/MultipleAnimationModel/MultipleAnimationModel.component';
import { spawnState } from 'src/state/spawnAtom';

export const CAMERA = {
TARGET: {
Expand Down Expand Up @@ -145,6 +146,8 @@ export interface AvatarProps extends LightingProps, EnvironmentProps, Omit<BaseM
* Use any three.js(fiber, post-processing) compatible components to render in the scene.
*/
children?: ReactNode;
animations?: Record<string, string>;
activeAnimation?: string;
}

/**
Expand All @@ -155,6 +158,8 @@ export interface AvatarProps extends LightingProps, EnvironmentProps, Omit<BaseM
const Avatar: FC<AvatarProps> = ({
modelSrc,
animationSrc = undefined,
animations = undefined,
activeAnimation = undefined,
poseSrc = undefined,
environment = 'soft',
halfBody = false,
Expand Down Expand Up @@ -199,6 +204,20 @@ const Avatar: FC<AvatarProps> = ({
return null;
}

if (!!activeAnimation && !halfBody && animations) {
return (
<MultipleAnimationModel
emotion={emotion}
modelSrc={modelSrc}
animations={animations}
activeAnimation={activeAnimation}
scale={scale}
onLoaded={onLoaded}
bloom={effects?.bloom}
/>
);
}

if (!!animationSrc && !halfBody && isValidFormat(animationSrc)) {
return (
<AnimationModel
Expand Down Expand Up @@ -244,7 +263,20 @@ const Avatar: FC<AvatarProps> = ({
return (
<StaticModel modelSrc={modelSrc} scale={scale} onLoaded={onLoaded} emotion={emotion} bloom={effects?.bloom} />
);
}, [halfBody, animationSrc, modelSrc, scale, poseSrc, idleRotation, emotion, onLoaded, headMovement, effects?.bloom]);
}, [
modelSrc,
activeAnimation,
halfBody,
animations,
animationSrc,
poseSrc,
scale,
onLoaded,
emotion,
effects?.bloom,
idleRotation,
headMovement
]);

useEffect(() => triggerCallback(onLoading), [modelSrc, animationSrc, onLoading]);

Expand Down
Loading

0 comments on commit d40a999

Please sign in to comment.