11 KiB
Animation & 3D Model System
This document describes how to use the 3D model components and animation system in La-Fabrik.
Table of Contents
- Model Types Overview
- SimpleModel - Static Models
- AnimatedModel - Animated Models
- Animation Control
- Other 3D Components
- 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.
import { SimpleModel } from "@/components/3d";
<SimpleModel
modelPath="/models/elecsimple/model.gltf"
position={[0, 0, -5]}
rotation={[0, 45, 0]}
scale={1}
castShadow={true}
receiveShadow={true}
/>;
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).
import { AnimatedModel, useAnimatedModel } from "@/components/3d";
// Basic usage
<AnimatedModel
modelPath="/models/elec/model.gltf"
defaultAnimation="Idle"
position={[0, 0, -5]}
rotation={[0, 0, 0]}
scale={0.01}
autoPlay={true}
speed={1}
fadeDuration={0.3}
/>;
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
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 (
<mesh onClick={() => play("Run", 0.5)}>
<boxGeometry />
</mesh>
);
}
// Usage
<AnimatedModel
modelPath="/models/elec/model.gltf"
defaultAnimation="Idle"
position={[0, 0, -5]}
scale={0.01}
>
<AnimationController />
</AnimatedModel>;
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
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 (
<group>
<mesh onClick={handleWalk} position={[-1, 0, 0]}>
<boxGeometry />
</mesh>
<mesh onClick={handleRun} position={[0, 0, 0]}>
<boxGeometry />
</mesh>
<mesh onClick={handleIdle} position={[1, 0, 0]}>
<boxGeometry />
</mesh>
</group>
);
}
Combined: GrabbableObject with Animation
You can combine AnimatedModel inside GrabbableObject to create animated objects that can be picked up:
import { AnimatedModel, GrabbableObject } from "@/components/3d";
// Animated weapon/tool that player can pick up
<GrabbableObject position={[0, 1, 0]} colliders="cuboid">
<AnimatedModel
modelPath="/models/sword/model.gltf"
defaultAnimation="Idle"
position={[0, 0, 0]}
scale={0.02}
autoPlay={true}
/>
</GrabbableObject>;
Or create an animated character that can be grabbed:
import {
AnimatedModel,
GrabbableObject,
useAnimatedModel,
} from "@/components/3d";
// Controller that triggers animations when grabbed
function AnimatedGrabber() {
const { play, fadeTo } = useAnimatedModel();
return (
<AnimatedModel
modelPath="/models/elec/model.gltf"
defaultAnimation="Idle"
position={[0, 0, 0]}
scale={0.01}
autoPlay={true}
/>
);
}
// When grabbed, play "Grab" animation
<GrabbableObject
position={[0, 1, 0]}
colliders="cuboid"
onGrab={() => {
// This would require a context or store to trigger
console.log("Object grabbed!");
}}
>
<AnimatedGrabber />
</GrabbableObject>;
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.
import { GrabbableObject } from "@/components/3d";
<GrabbableObject position={[0, 1, 0]} colliders="cuboid">
<mesh>
<boxGeometry args={[0.5, 0.5, 0.5]} />
<meshStandardMaterial color="red" />
</mesh>
</GrabbableObject>;
TriggerObject
Objects that trigger events when interacted with.
import { TriggerObject } from "@/components/3d";
<TriggerObject
position={[0, 1, 0]}
soundPath="/sounds/click.mp3"
onTrigger={() => console.log("Triggered!")}
>
<mesh>
<sphereGeometry />
<meshStandardMaterial color="blue" />
</mesh>
</TriggerObject>;
InteractableObject
Base object for interactions.
import { InteractableObject } from "@/components/3d";
<InteractableObject
position={[0, 1, 0]}
onInteract={() => console.log("Interacted!")}
>
<mesh>
<cylinderGeometry />
<meshStandardMaterial color="green" />
</mesh>
</InteractableObject>;
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.01for Mixamo-exported models - Scale
1for models in correct units
Cloning
SimpleModelusesscene.clone()for proper React lifecycleAnimatedModeluses the original scene directly to preserve SkinnedMesh + Armature structure
Animation System
The animation system uses:
@react-three/drei:useGLTFfor loading,useAnimationsfor animation control- Three.js:
AnimationMixerfor 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)