This commit is contained in:
math-pixel
2026-04-28 16:54:00 +02:00
parent 8abc69ebc3
commit 9ada4298c3
17 changed files with 17460 additions and 2 deletions
+12428
View File
File diff suppressed because it is too large Load Diff
+4580
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+234
View File
@@ -0,0 +1,234 @@
import {
createContext,
useContext,
useRef,
useState,
useEffect,
useCallback,
} from "react";
import { useGLTF, useAnimations } from "@react-three/drei";
import type { AnimationAction } from "three";
import * as THREE from "three";
import type { Vector3Tuple } from "@/types/3d";
export interface AnimatedModelConfig {
modelPath: string;
animations?: string[];
defaultAnimation?: string;
position?: Vector3Tuple;
rotation?: Vector3Tuple;
scale?: Vector3Tuple | number;
fadeDuration?: number;
speed?: number;
autoPlay?: boolean;
onLoaded?: () => void;
onAnimationEnd?: (animationName: string) => void;
}
interface AnimatedModelContextValue {
play: (name: string, fade?: number) => void;
stop: (fade?: number) => void;
fadeTo: (name: string, fade?: number) => void;
currentAnimation: string;
isReady: boolean;
setSpeed: (speed: number) => void;
names: string[];
}
const AnimatedModelContext = createContext<AnimatedModelContextValue | null>(
null,
);
export function useAnimatedModel(): AnimatedModelContextValue {
const context = useContext(AnimatedModelContext);
if (!context) {
throw new Error("useAnimatedModel must be used within AnimatedModel");
}
return context;
}
interface AnimatedModelProps extends AnimatedModelConfig {
children?: React.ReactNode;
}
export function AnimatedModel({
modelPath,
animations: _animations = [],
defaultAnimation = "Idle",
position = [0, 0, 0],
rotation = [0, 0, 0],
scale = 1,
fadeDuration = 0.3,
speed = 1,
autoPlay = true,
onLoaded,
onAnimationEnd,
children,
}: AnimatedModelProps): React.JSX.Element {
const groupRef = useRef<THREE.Group>(null);
const { scene, animations } = useGLTF(modelPath);
const { actions, names, mixer } = useAnimations(animations, groupRef);
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;
}
}, [mixer, speed]);
useEffect(() => {
const handleFinished = (e: { action: AnimationAction }) => {
const clipName = e.action.getClip().name;
onAnimationEnd?.(clipName);
};
if (mixer) {
mixer.addEventListener("finished", handleFinished);
return () => {
mixer.removeEventListener("finished", handleFinished);
};
}
}, [mixer, onAnimationEnd]);
const play = useCallback(
(name: string, fade = fadeDuration) => {
const action = actions[name];
if (action) {
Object.values(actions).forEach((a) => {
if (a && a !== action) a.fadeOut(fade);
});
action.reset().fadeIn(fade).play();
setCurrentAnim(name);
}
},
[actions, fadeDuration],
);
const stop = useCallback(
(fade = fadeDuration) => {
Object.values(actions).forEach((a) => a?.fadeOut(fade));
const defaultAction = actions[defaultAnimation];
if (defaultAction) {
defaultAction.reset().fadeIn(fade).play();
setCurrentAnim(defaultAnimation);
}
},
[actions, defaultAnimation, fadeDuration],
);
const fadeTo = useCallback(
(name: string, fade = fadeDuration) => {
const action = actions[name];
if (action) {
Object.values(actions).forEach((a) => {
if (a && a !== action) a.fadeOut(fade);
});
action.reset().fadeIn(fade).play();
setCurrentAnim(name);
}
},
[actions, fadeDuration],
);
const setSpeed = useCallback(
(newSpeed: number) => {
if (mixer) {
mixer.timeScale = newSpeed;
}
},
[mixer],
);
useEffect(() => {
if (autoPlay && names.length > 0) {
// Essayer d'abord l'animation par défaut, sinon la première disponible
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],
);
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");
}
} else if (names.length === 0) {
console.log("Aucune animation trouvée dans le modèle");
}
}, [actions, defaultAnimation, names, autoPlay, onLoaded]);
const parsedScale =
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
const contextValue: AnimatedModelContextValue = {
play,
stop,
fadeTo,
currentAnimation: currentAnim,
isReady,
setSpeed,
names,
};
return (
<AnimatedModelContext.Provider value={contextValue}>
<group
ref={groupRef}
position={position}
rotation={rotation}
scale={parsedScale}
>
<primitive object={scene.clone()} />
{children}
</group>
</AnimatedModelContext.Provider>
);
}
+42
View File
@@ -0,0 +1,42 @@
import { useGLTF } from "@react-three/drei";
import type { Vector3Tuple } from "@/types/3d";
export interface SimpleModelConfig {
modelPath: string;
position?: Vector3Tuple;
rotation?: Vector3Tuple;
scale?: Vector3Tuple | number;
castShadow?: boolean;
receiveShadow?: boolean;
}
interface SimpleModelProps extends SimpleModelConfig {
children?: React.ReactNode;
}
export function SimpleModel({
modelPath,
position = [0, 0, 0],
rotation = [0, 0, 0],
scale = 1,
castShadow = true,
receiveShadow = true,
children,
}: SimpleModelProps): React.JSX.Element {
const { scene } = useGLTF(modelPath);
const parsedScale =
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
return (
<group position={position} rotation={rotation} scale={parsedScale}>
{children ?? (
<primitive
object={scene.clone()}
castShadow={castShadow}
receiveShadow={receiveShadow}
/>
)}
</group>
);
}
+8
View File
@@ -0,0 +1,8 @@
export { AnimatedModel, useAnimatedModel } from "./AnimatedModel";
export type { AnimatedModelConfig } from "./AnimatedModel";
export { SimpleModel } from "./SimpleModel";
export type { SimpleModelConfig } from "./SimpleModel";
export { useCharacterAnimation } from "@/hooks/useCharacterAnimation";
export type { CharacterAnimationConfig } from "@/hooks/useCharacterAnimation";
+106
View File
@@ -0,0 +1,106 @@
import { useRef, useEffect, useState, useCallback } from "react";
import { useGLTF, useAnimations } from "@react-three/drei";
import type { AnimationAction, AnimationMixer } from "three";
import * as THREE from "three";
export interface CharacterAnimationConfig {
modelPath: string;
initialAnimation?: string;
fadeDuration?: number;
}
interface UseCharacterAnimationReturn {
scene: THREE.Group;
actions: { [key: string]: AnimationAction | null };
names: string[];
mixer: AnimationMixer;
groupRef: React.MutableRefObject<THREE.Group | null>;
currentAnimation: string;
play: (name: string) => void;
stop: () => void;
fadeTo: (name: string, duration?: number) => void;
setAnimationSpeed: (speed: number) => void;
}
const DEFAULT_FADE_DURATION = 0.3;
export function useCharacterAnimation(
config: CharacterAnimationConfig,
): UseCharacterAnimationReturn {
const {
modelPath,
initialAnimation = "Idle",
fadeDuration = DEFAULT_FADE_DURATION,
} = config;
const groupRef = useRef<THREE.Group>(null);
const { scene, animations } = useGLTF(modelPath);
const { actions, names, mixer } = useAnimations(animations, groupRef);
const [currentAnimation, setCurrentAnimation] = useState(initialAnimation);
const play = useCallback(
(name: string) => {
const action = actions[name];
if (action) {
Object.values(actions).forEach((a) => {
if (a && a !== action) a.fadeOut(fadeDuration);
});
action.reset().fadeIn(fadeDuration).play();
setCurrentAnimation(name);
}
},
[actions, fadeDuration],
);
const stop = useCallback(() => {
Object.values(actions).forEach((a) => a?.fadeOut(fadeDuration));
const defaultAction = actions[initialAnimation as string];
if (defaultAction) {
defaultAction.reset().fadeIn(fadeDuration).play();
setCurrentAnimation(initialAnimation);
}
}, [actions, initialAnimation, fadeDuration]);
const fadeTo = useCallback(
(name: string, duration = fadeDuration) => {
const targetAction = actions[name];
if (targetAction) {
Object.values(actions).forEach((a) => {
if (a && a !== targetAction) a.fadeOut(duration);
});
targetAction.reset().fadeIn(duration).play();
setCurrentAnimation(name);
}
},
[actions, fadeDuration],
);
const setAnimationSpeed = useCallback(
(speed: number) => {
if (mixer) {
mixer.timeScale = speed;
}
},
[mixer],
);
useEffect(() => {
const defaultAction = actions[initialAnimation as string];
if (defaultAction) {
defaultAction.play();
}
}, [actions, initialAnimation]);
return {
scene,
actions,
names,
mixer,
groupRef,
currentAnimation,
play,
stop,
fadeTo,
setAnimationSpeed,
};
}
+38 -2
View File
@@ -21,7 +21,28 @@ import {
TEST_SCENE_TRIGGER_SOUND_PATH, TEST_SCENE_TRIGGER_SOUND_PATH,
} from "@/data/testSceneConfig"; } from "@/data/testSceneConfig";
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode"; import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
import type { OctreeReadyHandler } from "@/types/3d"; 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 (
<primitive
object={scene.clone()}
position={[0, 0, -5]}
scale={[1, 1, 1]}
/>
);
}
interface TestSceneProps { interface TestSceneProps {
onOctreeReady: OctreeReadyHandler; onOctreeReady: OctreeReadyHandler;
@@ -84,7 +105,22 @@ export function TestScene({
/> />
</mesh> </mesh>
</TriggerObject> </TriggerObject>
</Physics> </Physics>
<AnimatedModel
modelPath={MODEL_PATH}
defaultAnimation="Idle"
position={[0, 0, -5]}
>
<AnimationTester />
</AnimatedModel>
{/* <SimpleModel
modelPath="/models/electricenne/model.gltf"
position={[0, 1, -5]}
scale={1}
/> */}
</> </>
); );
} }