Améliore le visuel de l'avatar Eve — lissage géométrique et couvertures de jointures

- Fusion de vertices plus agressive (corps 0.08, bras 0.07, tête 0.08)
- Matériau laque blanche unifié : emissiveIntensity 0.16, clearcoat 1.0
- Matériau tête dédié : emissiveIntensity 0.28 pour effacer les ombres aux seams
- Collar élargi (tube 0.14), cylindre de cou ajouté, épaulières agrandies (scale Z 0.56)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Sonnet 4.6 committed 2026-05-20 15:18:11 +02:00
commit d77ace03cd
169 files changed
+83033

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useFrame, useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import * as THREE from 'three';
import { EveModel } from './EveModel';
import { EveAvatar, EveEmotion, EmotionService } from '../services/emotionService';
interface AvatarModelLoaderProps {
avatarId: EveAvatar;
emotion: EveEmotion;
isSpeaking: boolean;
}
type AvatarManifest = Partial<Record<Exclude<EveAvatar, 'eve'>, string | null>>;
const AVATAR_MANIFEST_PATH = '/avatars/avatar-manifest.json';
const LoadedGlbAvatar: React.FC<{
path: string;
avatarId: EveAvatar;
emotion: EveEmotion;
isSpeaking: boolean;
}> = ({ path, avatarId, emotion, isSpeaking }) => {
const gltf = useLoader(GLTFLoader, path);
const scene = useMemo(() => gltf.scene.clone(true), [gltf.scene]);
useEffect(() => {
const style = EmotionService.getEmotionStyle(emotion, avatarId);
scene.traverse((child) => {
if (!(child instanceof THREE.Mesh)) return;
child.castShadow = false;
child.receiveShadow = false;
const materialName = Array.isArray(child.material)
? child.material.map((material) => material.name).join(' ')
: child.material?.name || '';
if (/eye|led|visor|screen|emissive/i.test(`${child.name} ${materialName}`)) {
const eyeMaterial = new THREE.MeshBasicMaterial({
color: new THREE.Color(style.eyeColor),
transparent: true,
opacity: 0.96
});
child.material = eyeMaterial;
}
});
}, [avatarId, emotion, scene]);
useFrame((state) => {
const t = state.clock.getElapsedTime();
scene.position.y = Math.sin(t * 1.4) * 0.05 - 0.28;
scene.rotation.y = Math.sin(t * 0.45) * 0.05;
if (isSpeaking) {
scene.rotation.z = Math.sin(t * 8) * 0.025;
} else {
scene.rotation.z = THREE.MathUtils.lerp(scene.rotation.z, 0, 0.12);
}
});
return <primitive object={scene} scale={1} />;
};
export const AvatarModelLoader: React.FC<AvatarModelLoaderProps> = ({ avatarId, emotion, isSpeaking }) => {
const [assetPath, setAssetPath] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setAssetPath(null);
if (avatarId === 'eve') return;
fetch(AVATAR_MANIFEST_PATH, { cache: 'no-store' })
.then((response) => {
if (!response.ok) return null;
return response.json() as Promise<AvatarManifest>;
})
.then((manifest) => {
if (cancelled || !manifest) return;
const nextAssetPath = manifest[avatarId];
setAssetPath(typeof nextAssetPath === 'string' && nextAssetPath.trim() ? nextAssetPath : null);
})
.catch(() => {
if (!cancelled) setAssetPath(null);
});
return () => {
cancelled = true;
};
}, [avatarId]);
if (avatarId !== 'eve' && assetPath) {
return <LoadedGlbAvatar path={assetPath} avatarId={avatarId} emotion={emotion} isSpeaking={isSpeaking} />;
}
return <EveModel emotion={emotion} isSpeaking={isSpeaking} avatarId="eve" />;
};