diff --git a/animation.md b/animation.md new file mode 100644 index 0000000..3bbd2b2 --- /dev/null +++ b/animation.md @@ -0,0 +1,338 @@ +# Animation & 3D Model System + +This document describes how to use the 3D model components and animation system in La-Fabrik. + +## Table of Contents + +1. [Model Types Overview](#model-types-overview) +2. [SimpleModel - Static Models](#simplemodel---static-models) +3. [AnimatedModel - Animated Models](#animatedmodel---animated-models) +4. [Animation Control](#animation-control) +5. [Other 3D Components](#other-3d-components) +6. [Technical Notes](#technical-notes) + +--- + +## Model Types Overview + +The project provides three main types of model instantiation: + +| Type | Component | Use Case | +| ----------- | -------------------------------------------------------- | -------------------------------------------- | +| Static | `SimpleModel` | Props, decoration, objects without animation | +| Animated | `AnimatedModel` | Characters, animated objects with skeleton | +| Interactive | `GrabbableObject`, `TriggerObject`, `InteractableObject` | Objects player can interact with | + +--- + +## SimpleModel - Static Models + +Use for GLTF models **without** skeleton/armature and no animations. + +```tsx +import { SimpleModel } from "@/components/3d"; + +; +``` + +### Props + +| Prop | Type | Default | Description | +| --------------- | ------------------------ | ----------- | --------------------------------- | +| `modelPath` | `string` | required | Path to GLTF file in `/public` | +| `position` | `Vector3Tuple` | `[0, 0, 0]` | World position [x, y, z] | +| `rotation` | `Vector3Tuple` | `[0, 0, 0]` | Rotation in degrees [x, y, z] | +| `scale` | `number \| Vector3Tuple` | `1` | Scale factor or [x, y, z] | +| `castShadow` | `boolean` | `true` | Enable shadow casting | +| `receiveShadow` | `boolean` | `true` | Enable shadow receiving | +| `children` | `ReactNode` | - | Child components to render inside | + +--- + +## AnimatedModel - Animated Models + +Use for GLTF models **with** skeleton/armature and animations (like Mixamo characters). + +```tsx +import { AnimatedModel, useAnimatedModel } from "@/components/3d"; + +// Basic usage +; +``` + +### Props + +| Prop | Type | Default | Description | +| ------------------ | ------------------------ | ----------- | --------------------------------------------- | +| `modelPath` | `string` | required | Path to GLTF file in `/public` | +| `defaultAnimation` | `string` | `"Idle"` | Animation name to play by default | +| `animations` | `string[]` | `[]` | List of animation names (optional) | +| `position` | `Vector3Tuple` | `[0, 0, 0]` | World position [x, y, z] | +| `rotation` | `Vector3Tuple` | `[0, 0, 0]` | Rotation in degrees [x, y, z] | +| `scale` | `number \| Vector3Tuple` | `1` | Scale factor | +| `autoPlay` | `boolean` | `true` | Auto-play default animation | +| `speed` | `number` | `1` | Animation playback speed | +| `fadeDuration` | `number` | `0.3` | Transition duration in seconds | +| `onLoaded` | `() => void` | - | Callback when model loads | +| `onAnimationEnd` | `(name: string) => void` | - | Callback when animation ends | +| `children` | `ReactNode` | - | Child components (can use `useAnimatedModel`) | + +### Important: Scale + +Animated models (like Mixamo exports) often need a small scale (e.g., `0.01`) because they are exported in meters while Three.js uses different units. Adjust until the model appears at the right size. + +--- + +## Animation Control + +To control animations from inside or outside the `AnimatedModel`, use the `useAnimatedModel` hook. + +### Basic Control + +```tsx +import { AnimatedModel, useAnimatedModel } from "@/components/3d"; + +// Create a controller component to use inside AnimatedModel +function AnimationController() { + const { play, stop, fadeTo, currentAnimation, names, setSpeed, isReady } = + useAnimatedModel(); + + // names contains all available animation names + // currentAnimation is the name of the currently playing animation + // isReady is true when model and animations are loaded + + return ( + play("Run", 0.5)}> + + + ); +} + +// Usage + + +; +``` + +### Available Methods + +| Method | Signature | Description | +| ------------------ | --------------------------------------- | ------------------------------------ | +| `play` | `(name: string, fade?: number) => void` | Play animation with optional fade | +| `fadeTo` | `(name: string, fade?: number) => void` | Fade to another animation | +| `stop` | `(fade?: number) => void` | Stop and return to default animation | +| `setSpeed` | `(speed: number) => void` | Set animation speed | +| `currentAnimation` | `string` | Current animation name (getter) | +| `names` | `string[]` | Available animation names | +| `isReady` | `boolean` | Whether model is loaded | + +### Transition Example + +```tsx +function Character() { + const { play, fadeTo, currentAnimation } = useAnimatedModel(); + + const handleWalk = () => fadeTo("Walk", 0.5); // 0.5s fade + const handleRun = () => play("Run", 0.3); // 0.3s fade + const handleIdle = () => play("Idle", 0.5); // return to idle + + return ( + + + + + + + + + + + + ); +} +``` + +### Combined: GrabbableObject with Animation + +You can combine `AnimatedModel` inside `GrabbableObject` to create animated objects that can be picked up: + +```tsx +import { AnimatedModel, GrabbableObject } from "@/components/3d"; + +// Animated weapon/tool that player can pick up + + +; +``` + +Or create an animated character that can be grabbed: + +```tsx +import { + AnimatedModel, + GrabbableObject, + useAnimatedModel, +} from "@/components/3d"; + +// Controller that triggers animations when grabbed +function AnimatedGrabber() { + const { play, fadeTo } = useAnimatedModel(); + + return ( + + ); +} + +// When grabbed, play "Grab" animation + { + // This would require a context or store to trigger + console.log("Object grabbed!"); + }} +> + +; +``` + +**Note:** For complex interactions (like playing specific animations when grabbing), you'll need to connect the grab events to animation controls via a state manager or context. + +--- + +## Other 3D Components + +### GrabbableObject + +Objects that can be picked up by the player. + +```tsx +import { GrabbableObject } from "@/components/3d"; + + + + + + +; +``` + +### TriggerObject + +Objects that trigger events when interacted with. + +```tsx +import { TriggerObject } from "@/components/3d"; + + console.log("Triggered!")} +> + + + + +; +``` + +### InteractableObject + +Base object for interactions. + +```tsx +import { InteractableObject } from "@/components/3d"; + + console.log("Interacted!")} +> + + + + +; +``` + +--- + +## Technical Notes + +### GLTF Models + +- Models should be placed in `/public/models/` +- Supported formats: `.gltf`, `.glb` +- Animated models must have an Armature/skeleton for animations to work + +### Model Scale Issue + +If animated models don't appear, they may be too small or too large. Try: + +- Scale `0.01` for Mixamo-exported models +- Scale `1` for models in correct units + +### Cloning + +- `SimpleModel` uses `scene.clone()` for proper React lifecycle +- `AnimatedModel` uses the original scene directly to preserve SkinnedMesh + Armature structure + +### Animation System + +The animation system uses: + +- `@react-three/drei`: `useGLTF` for loading, `useAnimations` for animation control +- Three.js: `AnimationMixer` for playback + +### No State Machine + +This system intentionally avoids complex state machines (like Unity's Animator). For simple animation transitions, use the `play`, `fadeTo`, and `stop` methods directly. + +--- + +## File Structure + +``` +src/ +├── components/3d/ +│ ├── AnimatedModel.tsx # Animated model component + context +│ ├── SimpleModel.tsx # Static model component +│ ├── GrabbableObject.tsx # Pickable object +│ ├── TriggerObject.tsx # Trigger event object +│ ├── InteractableObject.tsx +│ └── index.ts # Central exports +└── hooks/ + └── useCharacterAnimation.ts # Animation hook (legacy) +``` diff --git a/public/models/elec/model.bin b/public/models/elec/model.bin index 72c4426..96e13e8 100644 Binary files a/public/models/elec/model.bin and b/public/models/elec/model.bin differ diff --git a/public/models/elec/model.gltf b/public/models/elec/model.gltf index 2d17846..fc3c84f 100644 --- a/public/models/elec/model.gltf +++ b/public/models/elec/model.gltf @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ceb25ce3388e0a5f4bef3284ad91d0492f2fe4abd803c803ea5f8db509616c6e -size 46786 +oid sha256:35129131d3f1d70b9648b5ea09d704ffecaab6b52aa03c0ab32b476349b25f92 +size 47180 diff --git a/src/components/3d/AnimatedModel.tsx b/src/components/3d/AnimatedModel.tsx index 1c29105..390fa6f 100644 --- a/src/components/3d/AnimatedModel.tsx +++ b/src/components/3d/AnimatedModel.tsx @@ -66,48 +66,14 @@ export function AnimatedModel({ children, }: AnimatedModelProps): React.JSX.Element { const groupRef = useRef(null); + + void groupRef; const { scene, animations } = useGLTF(modelPath); - const { actions, names, mixer } = useAnimations(animations, groupRef); + const { actions, names, mixer } = useAnimations(animations, scene); + const [currentAnim, setCurrentAnim] = useState(defaultAnimation); const [isReady, setIsReady] = useState(false); - // DEBUG: Analyser la structure du modèle - useEffect(() => { - console.log("=== DEBUG ANIMATED MODEL ==="); - console.log("modelPath:", modelPath); - console.log("scene:", scene); - console.log("scene.children.length:", scene.children.length); - console.log( - "scene.children types:", - scene.children.map((c) => c.type), - ); - - let foundMesh = false; - let foundSkeleton = false; - - scene.traverse((child: THREE.Object3D) => { - if (child.type === "SkinnedMesh") { - console.log("✅ Found SkinnedMesh:", child.name); - console.log(" visible:", child.visible); - console.log(" skeleton:", (child as THREE.SkinnedMesh).skeleton); - foundMesh = true; - } - if (child.type === "Mesh") { - console.log("✅ Found Mesh:", child.name); - console.log(" visible:", child.visible); - foundMesh = true; - } - if (child.type === "Skeleton") { - console.log("✅ Found Skeleton:", child); - foundSkeleton = true; - } - }); - - if (!foundMesh) console.log("❌ AUCUN MESH TROUVÉ!"); - if (!foundSkeleton) console.log("❌ AUCUN SKELETON TROUVÉ!"); - console.log("========================="); - }, [scene, modelPath]); - useEffect(() => { if (mixer) { mixer.timeScale = speed; @@ -179,35 +145,30 @@ export function AnimatedModel({ useEffect(() => { if (autoPlay && names.length > 0) { - // Essayer d'abord l'animation par défaut, sinon la première disponible + console.log(`[AnimatedModel] Available animations: ${names.join(", ")}`); + let defaultAction = actions[defaultAnimation as string]; - // Si l'animation par défaut n'existe pas, utiliser la première disponible if (!defaultAction && names.length > 0) { console.log( - `Animation "${defaultAnimation}" non trouvée, utilisation de:`, - names[0], + `[AnimatedModel] "${defaultAnimation}" not found, using: ${names[0]}`, ); defaultAction = actions[names[0] as string]; } if (defaultAction) { - console.log("Lecture de l'animation:", defaultAction.getClip().name); defaultAction.play(); setIsReady(true); setCurrentAnim(defaultAction.getClip().name); onLoaded?.(); } else { - console.log("Aucune animation disponible dans les actions"); + console.log("[AnimatedModel] No available animation in actions"); } } else if (names.length === 0) { - console.log("Aucune animation trouvée dans le modèle"); + console.log("[AnimatedModel] No animation found in model"); } }, [actions, defaultAnimation, names, autoPlay, onLoaded]); - const parsedScale = - typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale; - const contextValue: AnimatedModelContextValue = { play, stop, @@ -218,17 +179,23 @@ export function AnimatedModel({ names, }; + // Apply transforms to scene directly + useEffect(() => { + scene.position.set(...position); + scene.rotation.set( + (rotation[0] * Math.PI) / 180, + (rotation[1] * Math.PI) / 180, + (rotation[2] * Math.PI) / 180, + ); + const s = + typeof scale === "number" ? [scale, scale, scale] : (scale ?? [1, 1, 1]); + scene.scale.set(s[0] ?? 1, s[1] ?? 1, s[2] ?? 1); + }, [scene, position, rotation, scale]); + return ( - - - {children} - + + {children} ); } diff --git a/src/world/debug/TestScene.tsx b/src/world/debug/TestScene.tsx index e7d9bba..7452329 100644 --- a/src/world/debug/TestScene.tsx +++ b/src/world/debug/TestScene.tsx @@ -1,8 +1,9 @@ import { useRef } from "react"; -import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier"; import * as THREE from "three"; +import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier"; import { GrabbableObject } from "@/components/3d/GrabbableObject"; import { TriggerObject } from "@/components/3d/TriggerObject"; +import { AnimatedModel } from "@/components/3d"; import { TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS, TEST_SCENE_FLOOR_POSITION, @@ -21,28 +22,7 @@ import { TEST_SCENE_TRIGGER_SOUND_PATH, } from "@/data/testSceneConfig"; import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode"; -import type { OctreeReadyHandler } from "@/types/3d";;import { SimpleModel } from "@/components/3d"; - -import { useGLTF } from "@react-three/drei"; -// Dans votre composant - - -// --- -import { AnimatedModel, useAnimatedModel } from "@/components/3d"; - -const MODEL_PATH = "/models/elec/model.gltf"; - -function AnimationTester(): React.JSX.Element { - const { scene } = useGLTF("/models/elec/model.gltf"); -return ( - -); -} - +import type { OctreeReadyHandler } from "@/types/3d"; interface TestSceneProps { onOctreeReady: OctreeReadyHandler; @@ -105,22 +85,14 @@ export function TestScene({ /> - - - + - - - {/* */} + modelPath="/models/elec/model.gltf" + defaultAnimation="Idle" + position={[0, 0, -5]} + scale={1} + /> ); -} \ No newline at end of file +}