Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/smooth animations #92

Merged
merged 16 commits into from
Sep 23, 2024
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
Loading