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
+}