Skip to content

Commit

Permalink
feat: half body and full body dynamic position update
Browse files Browse the repository at this point in the history
  • Loading branch information
andrepat0 committed Nov 7, 2024
1 parent d36b0de commit 05c0c4e
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FullbodyAvatar } from './components/FullbodyAvatar/FullbodyAvatar';
import HalfBodyAvatar from './components/halfbodyAvatar';
import PositionControls from './positionControls/positionControls';
import { PerspectiveCamera, Vector3 } from 'three';
import { getLocalConfig } from '../../../../helpers/configuration';

interface Props {
showControls: boolean;
Expand All @@ -23,8 +24,6 @@ interface Props {
updateCurrentViseme: (
currentTime: number
) => { name: string; weight: number } | null;
enablePositionControls?: boolean;
setEnablePositionControls?: (value: boolean) => void;
setCameraZ: (value: number) => void;
}

Expand Down Expand Up @@ -72,10 +71,10 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
halfBody,
loading,
isZoomed,
avatarHeight,
avatarDepth,
updateCurrentViseme,
resetVisemeQueue,
enablePositionControls,
setEnablePositionControls,
setCameraZ,
}) => {
const [currentBaseAction, setCurrentBaseAction] = useState({
Expand All @@ -95,9 +94,6 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({

const [timeScale, setTimeScale] = useState(0.8);

const [avatarHeight, setAvatarHeight] = useState(0);
const [avatarDepth, setAvatarDepth] = useState(0);

// Set the morph target influences for the given emotions
const setEmotionMorphTargetInfluences = useCallback((action: string) => {
if (
Expand Down Expand Up @@ -217,25 +213,15 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
modifyTimeScale={modifyTimeScale}
/>
)}
{
enablePositionControls && (
<PositionControls
avatarHeight={avatarHeight}
avatarDepth={avatarDepth}
setAvatarHeight={setAvatarHeight}
setAvatarDepth={setAvatarDepth}
/>
)
}
{halfBody ? (
<HalfBodyAvatar
url={url}
onCameraZChange={setCameraZ}
setMorphTargetInfluences={setMorphTargetInfluences}
setMorphTargetDictionary={setMorphTargetDictionary}
updateCurrentViseme={updateCurrentViseme}
avatarHeight={avatarHeight}
avatarDepth={avatarDepth}
avatarHeight={avatarHeight || 50}
avatarDepth={avatarDepth || -50}
/>
) : (
<FullbodyAvatar
Expand All @@ -254,8 +240,8 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
emotionMorphTargets={emotionMorphTargets}
halfBody={halfBody}
onCameraZChange={setCameraZ}
avatarHeight={avatarHeight}
avatarDepth={avatarDepth}
avatarHeight={avatarHeight || 50}
avatarDepth={avatarDepth || -50}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import React, { useEffect, useRef, useMemo, useCallback } from 'react';
import { useEffect, useRef, useMemo } from 'react';
import {
Vector3,
Euler,
AnimationMixer,
SkinnedMesh,
Object3D,
MathUtils,
AnimationAction,
LoopOnce,
} from 'three';
import { useAnimations, useGLTF } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { AnimationState, FullbodyAvatarProps } from './types';
import { AnimationController } from './AnimationController';
import { MorphTargetController } from './MorhTargetController';
import { MorphTargetController } from '../MorphTargetController';
import { AvatarPositionController } from '../PositionController';
import {
AVATAR_POSITION,
AVATAR_ROTATION,
AVATAR_POSITION_ZOOMED,
ANIMATION_URLS,
DEFAULT_CONFIG,
} from './constants';
SCALE_LERP_FACTOR,
} from '../constants';

export default function FullbodyAvatar({
export function FullbodyAvatar({
url,
sex,
currentBaseAction,
Expand All @@ -33,24 +31,68 @@ export default function FullbodyAvatar({
setMorphTargetDictionary,
setMorphTargetInfluences,
emotionMorphTargets,
avatarHeight = 50,
avatarDepth = 0,
onCameraZChange,
}: FullbodyAvatarProps) {
const { scene } = useGLTF(url);
const { animations } = useGLTF(ANIMATION_URLS[sex]);
const { actions } = useAnimations(animations, scene);

const animationControllerRef = useRef<AnimationController>();
const morphTargetControllerRef = useRef<MorphTargetController>();
const positionControllerRef = useRef<AvatarPositionController>();

const blinkStateRef = useRef({
isBlinking: false,
lastBlinkTime: 0,
nextBlinkTime: 0,
blinkStartTime: 0,
});

// Initialize controllers
useEffect(() => {
if (!positionControllerRef.current) {
positionControllerRef.current = new AvatarPositionController(AVATAR_POSITION);
}

if (!actions || !scene) return;

const mixer = new AnimationMixer(scene);
animationControllerRef.current = new AnimationController(
mixer,
actions as Record<string, AnimationAction>,
{ ...DEFAULT_CONFIG }
);

if (headMesh) {
morphTargetControllerRef.current = new MorphTargetController(headMesh);

if (headMesh.morphTargetDictionary && headMesh.morphTargetInfluences) {
setMorphTargetDictionary(headMesh.morphTargetDictionary);
const initialInfluences = Object.keys(headMesh.morphTargetDictionary)
.reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
setMorphTargetInfluences(initialInfluences);
}
}
}, [actions, scene]);
useEffect(() => {
if (positionControllerRef.current) {
positionControllerRef.current.updateHeight(avatarHeight, false);
}
}, [avatarHeight]);

useEffect(() => {
if (positionControllerRef.current && onCameraZChange) {
const newCameraZ = positionControllerRef.current.updateDepth(avatarDepth, false);
onCameraZChange(newCameraZ);
}
}, [avatarDepth, onCameraZChange]);

// Find head mesh
const headMesh = useMemo(() => {
let foundMesh: SkinnedMesh | undefined;
scene.traverse((object: Object3D) => {
scene?.traverse((object: Object3D) => {
if (
object instanceof SkinnedMesh &&
(object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar')
Expand All @@ -61,8 +103,8 @@ export default function FullbodyAvatar({
return foundMesh;
}, [scene]);

// Initialize controllers
useEffect(() => {
// Initialize controllers
useEffect(() => {
if (!actions || !headMesh) return;

const mixer = new AnimationMixer(scene);
Expand Down Expand Up @@ -116,12 +158,13 @@ export default function FullbodyAvatar({
animationControllerRef.current?.setTimeScale(timeScale);
}, [timeScale]);

// Animation update loop
useFrame(state => {

// Animation and scaling update loop
useFrame((state, delta) => {
const currentTime = state.clock.elapsedTime * 1000;

// Update animations
animationControllerRef.current?.update(state.clock.getDelta());
animationControllerRef.current?.update(delta);

// Update morph targets
if (morphTargetControllerRef.current) {
Expand All @@ -134,14 +177,23 @@ export default function FullbodyAvatar({
blinkStateRef.current
);
}

// Update scale with smooth transition
if (scene && positionControllerRef.current) {
const newScale = positionControllerRef.current.updateScale(SCALE_LERP_FACTOR);
scene.scale.copy(newScale);
}
});

// Get current position from controller
const position = positionControllerRef.current?.getPosition() || AVATAR_POSITION;

return (
<group
position={isZoomed ? AVATAR_POSITION_ZOOMED : AVATAR_POSITION}
position={position}
rotation={AVATAR_ROTATION}
>
<primitive object={scene} />
</group>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export class AvatarPositionController {
private mapHeightToScale(sliderValue: number, isHalfBody: boolean): number {
// Convert slider value to scale factor
if (isHalfBody) {
return MathUtils.lerp(1.6, 3, sliderValue / 100);
return MathUtils.lerp(1.4, 1.8, sliderValue / 100);
} else {
return MathUtils.lerp(0, 1.5, sliderValue / 100);
return MathUtils.lerp(0.5, 1.5, sliderValue / 100);
}
}

Expand Down
Loading

0 comments on commit 05c0c4e

Please sign in to comment.