Le personnage créé dans Blender est déformé dans Three.js — Retour sur la mise en œuvre de personnages 3D
Enregistrement des problèmes rencontrés lors de l'intégration de personnages dans Three.js pour le projet funda et mon portfolio personnel
Enregistrement des problèmes rencontrés lors de l'intégration de personnages dans Three.js pour le projet funda et mon portfolio personnel
Le mot de passe saisi sert à ouvrir, modifier et supprimer les commentaires secrets.
Pourquoi un personnage était-il nécessaire ?
De la modélisation au rigging
Apparence du rendu en low poly, car la subdivision pour adoucir le maillage n'est pas appliquée
La magie de l’ombrage imitant la haute résolution
Remplacer la réfraction par le retouche de texture
Réduction des appels de dessin
TL;DR
Comprendre les limites du moteur : Une structure parfaite dans Blender n'est pas forcément la solution finale dans Three.js.
Rigging centré sur le résultat : Le rigging n’est pas simplement « importer » mais transformer en « données de résultat pour le moteur ».
Centré sur la conception : Tous les critères de décision doivent se baser sur « comment fonctionner efficacement en runtime web ».
Les portfolios des deux projets mentionnés dans cet article peuvent être consultés sur Funda, Chaen.
En avançant sur le projet FUNDA, nous avions besoin d'un moyen de refléter visuellement les valeurs fondamentales du service, à savoir le « fun » et la « gamification ». Au-delà d'une simple décoration d'écran, le personnage était conçu comme un élément de l'UX qui encourage l'interaction émotionnelle avec l'utilisateur et motive l'apprentissage.
Le personnage a été choisi selon trois critères.
Initialement, nous avons pensé à un panda, dont le nom rappelle « Funda ». Le panda était parfait pour attraper le regard sur la mignonnerie, mais il semblait plus approprié à une catégorie « détente » qu'à « intelligent » ou « apprentissage », donc mis en attente.
Ensuite, nous avons envisagé la loutre, car sa forme anthropomorphique semble mieux adaptée à l'animation, elle est donc restée parmi les candidats.
Enfin, le renard avait une image intelligente qui se mariait bien avec le développeur et l'apprentissage, mais sa couleur orange entrait en conflit avec celle de la marque, nous avons finalement opté pour le renard polaire.
![]() | ![]() | ![]() |
|---|
Avant de commencer les travaux 3D, nous avons fait des croquis simples et des réalisations 2D pour définir l'impression et les postures du personnage. Bien que le résultat 2D ne semble pas poser de problème majeur pour l'exploitation du service en soi, nous avons reçu des retours indiquant qu’il manquait un petit quelque chose en termes de qualité.
Nous avons beaucoup réfléchi à la balance entre le temps de travail (ressources) et la qualité du rendu, mais avons finalement jugé que le personnage était le point clé de ce projet. Si c'était un facteur déterminant pour le succès du projet, il était logique de consacrer plus de temps à assurer une qualité certaine, d'où le défi lancé pour créer un personnage 3D.
J'ai progressé en me basant sur l'expérience de deux mises en œuvre de personnages humains. J'ai ajusté plusieurs fois les déformations pour affiner les détails. J'ai principalement utilisé le bruit pour le texturage, mais pour les parties nécessitant des détails, j'ai utilisé des ressources de Poly Haven.
| 1ère tête | 2ème défo | 3ème défo |
|---|---|---|
![]() | ![]() | ![]() |
En particulier, j'ai porté une attention particulière aux yeux. Pour capturer le mystère caractéristique du renard arctique, j'ai combiné le violet et le blanc, et utilisé « Transmission » et « Glass BSDF » pour créer un effet de réfraction ressemblant à du verre. Cependant, cette structure de nœuds est devenue un grand défi technique dans l'environnement Three.js. (développé ci-après)

Le rigging a été réalisé en utilisant Rigify, basé sur une structure tête d'animal + corps humain. J'ai également appliqué le « Damped Track » pour donner du mouvement à l'écharpe et à la queue.
Les expressions faciales ont été contrôlées en attribuant des Shape Key à chacune des composants : maillage du visage, cils, sourcils, langue, et dents.
| rigging corporel | expression faciale |
|---|---|
![]() | ![]() |
Dans Blender, tout fonctionnait parfaitement et il ne restait plus qu'à extraire le fichier. Le problème est survenu après cela.
En chargeant sur le web, j'ai constaté que les modificateurs n'étaient pas appliqués au rendu. Three.js ne prend pas en charge le calcul des modificateurs en temps réel pour chaque frame. J'ai dû retourner sur Blender pour appliquer les modificateurs, ce qui a entraîné une erreur indiquant que les maillages avec Shape Key ne peuvent pas appliquer correctement le Subdivision Modifier.
![]() | ![]() |
|---|
Au lieu d’augmenter aveuglément les faces, nous avons utilisé la Normalisation pondérée et la stratégie High Poly Normal Map Baking.
La méthode précédente de couvrir le globe oculaire par une pellicule de verre était trop lourde. Cette fois, nous avons fusionné le maillage de l’œil en un seul et incorporé les détails dans la texture. L'emplacement laissé par l'absence de calculs de réfraction a été compensé par l'ajout manuel de reflets et de profondeur à l'aide de Photoshop.
Cette approche visait à réduire la charge de rendu tout en préservant la satisfaction visuelle.

Ayant déjà recréé les Shape Key au moins cinq fois, je manquais de temps pour recommencer depuis le début et ai donc utilisé par force un plugin externe (SKKeeper). Lors de l'exportation vers Three.js, plusieurs messages d'erreur signalant la disparition des Shape Key ont été rencontrés, mais aucun problème n'était visible à l'écran et les modificateurs semblaient bien appliqués. Cependant, ne pas suivre la méthode correcte laissait une insatisfaction.
La structure IK et les contraintes basées sur Rigify ont été complètement ignorées par Three.js. Aucun mouvement intentionné n'a été réalisé. Bien qu'il soit compréhensible que le seul exportation des os de déformation ne suffise pas, même l'exportation de tous les os MCH, ORG, DEF pour les reconnaître et les contrôler ne donnait aucun résultat en termes de mouvement.
Dans Blender, il était possible de contrôler en temps réel les os grâce aux contrôleurs. En revanche, dans Three.js, les contrôleurs ont été complètement ignorés.
J'ai essayé de prendre le rig de Rigify pour le contrôler directement, mais cela n'a pas fonctionné comme prévu. Après plusieurs tentatives d'extraction du rig, j'ai pris beaucoup de temps et décidé de créer cinq scripts pour les déboguer dans l'ordre.
Résultat : Tous les rigs sont présents, ils sont mappés, et les valeurs de coordonnées bougent, mais cela n'est pas reflété à l'écran.
En conclusion, j'ai constaté que Rigify est trop complexe pour être contrôlé directement dans Three.js. Seuls les éléments comme les yeux, sourcils, et langue, sans connexion directe à Rigify mais parentés à l'os de la tête, pouvaient être déplacés.
Lors de la mise en œuvre des « yeux », qui déterminent la vitalité du personnage, un goulet d'étranglement technique est apparu lorsque la conception complexe des nœuds dans Blender ne fonctionnait pas comme prévu dans un moteur Web (Three.js).
Cependant, une fois exporté en GLB et vérifié dans Three.js, l'intention dans Blender a été complètement perdue.
Étant donné que la simple cuisson d'images ne pouvait pas reproduire la texture physique du verre sur le web, nous avons choisi d'injecter directement MeshPhysicalMaterial à l'exécution.
MeshPhysicalMaterial propose dans Three.js le rendu du matériau le plus physiquement précis. En ajustant directement des paramètres tels que IOR (indice de réfraction), Transmission, Coat, nous avons pu reproduire l'effet du matériau Glass de Blender à l'exécution. Bien que cela ne soit pas parfaitement identique, l'effet mystérieux voulu pour les yeux a été largement réalisé.
| avant | après |
|---|---|
![]() | ![]() |
En supprimant le rigging basé sur les contraintes, nous avons bake toutes les animations en données de keyframes image par image. Les animations ont été organisées dans Dope Sheet et Action Editor, puis poussées dans le NLA (Non-Linear Animation) Editor pour gérer les pistes. Pour déclencher des animations spécifiques dans le moteur, les noms des clips devaient être clairs, alors nous avons raffiné et bake tous les noms d'action avant l'extraction.
Grâce à cela, nous avons pu contrôler les animations sans confusion dans le code, par exemple avec actions['Greeting'].play().
Ce n'est qu'après avoir séparé ces deux éléments que l'interaction a été rendue possible.
useFixSkinnedMeshAprès l'application forcée de Subdivision, de nouveaux problèmes sont apparus dans Three.js.
Pour résoudre ces problèmes, nous avons créé un hook personnalisé.
Au cours du projet, nous avons ajouté des animations telles que le suivi du regard et la chute lors d'un clic. Le point de réflexion principal était jusqu'où aller dans le bake ?
Baker tous les mouvements dans Blender simplifie l'implémentation. Cependant, cela en fait un simple 'GIF qui se joue uniquement au clic' sans interaction en temps réel. Ce que je désirais était de réutiliser de manière flexible différentes combinaisons d'expressions selon la situation.
Nous avons donc élaboré une stratégie hybride :
Ensuite, une composant contrôleur distinct et une page d'expérience de personnage ont été construits pour permettre à l'utilisateur de manipuler le contrôleur et d'expérimenter en temps réel les différentes animations et changements d'expressions du personnage.
Avec l'adoption de la méthode hybride, des conflits entre des contrôles différents (données d'animation vs coordonnées de souris en temps réel) ont émergé, nécessitant l'application de deux exceptions pour les résoudre.
Si le regard suit la souris pendant la lecture d'une animation spécifique (ex : chute, salutation), un conflit se produit entre la valeur de rotation de l'os définie par les données d'animation et la logique lookAt en temps réel, entraînant un écran qui tremble ou des mouvements non naturels. Pour éviter cela, nous avons conçu un drapeau pour bloquer temporairement la logique de suivi du regard pendant l'exécution de l'animation active.
Les expressions peuvent se chevaucher. Par exemple, en changeant l'expression 'sourire' avec un poids de 1 pour 'colère' à 1, le maillage peut se déformer bizarrement. Pour éviter cela, lors du passage à un nouveau groupe d'expressions, la logique consistait à réinitialiser précisément les influences Shape Key de l'ancien groupe à 0, avant d'injecter la nouvelle valeur.
La structure impliquait l'intégration de personnages 3D sur de nombreuses pages (combat, connexion, écran des résultats, etc.), ce qui rendait la performance mobile essentielle. Diverses approches ont été utilisées pour éviter la baisse de frames (diminution de FPS) sur les appareils à faible spécification.
requestAnimationFrameLa boucle de rendu par défaut de Three.js a été construite sur la base de requestAnimationFrame. Cette méthode exécute le rappel juste avant que le navigateur n'affiche la prochaine frame, ce qui s'harmonise naturellement avec le taux de rafraîchissement du moniteur (60Hz, 120Hz, etc.). Lorsque l'onglet est déplacé en arrière-plan, l'exécution s'arrête automatiquement pour réduire les calculs inutiles.
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))Les appareils mobiles récents peuvent avoir une densité de pixels (DPR) atteignant 3 à 4. Plus le DPR est élevé, plus le nombre de pixels à traiter par le GPU augmente au carré. En limitant le DPR à 2, nous avons assuré une performance visuelle sans grande différence.
antialias: falseL'anticrénelage réduit l'effet de crénelage mais impose une charge importante au GPU. Dans l'environnement mobile, le DPR étant déjà élevé, l'effet de crénelage est moins perceptible. La désactivation n'entraîne pas de différence notable en qualité ressentie, elle a donc été désactivée courageusement.
En travaillant dans Blender, des pistes d'animation temporaires comme [Action Stash] s'accumulent dans l'éditeur NLA. Si elles sont incluses lors de l'export GLB, la taille du fichier augmente inutilement et Three.js gaspille de la mémoire en analysant toutes les pistes lors du chargement. Par conséquent, toutes les pistes inutilisées ont été supprimées avant l'exportation.
Les shaders procéduraux comme Principled BSDF, Noise, Gradient de Blender ne peuvent pas être calculés en temps réel dans Three.js. En cuisant (baking) ces shaders en textures d'image avant l'export GLB, Three.js n'a qu'à appliquer directement les images textures. Cela réduit considérablement le coût des calculs d'éclairage en temps réel et des shaders.
Cibles de baking : Color, Roughness, Normal, Alpha maps
SkeletonUtils.cloneUtilisant le même personnage sur plusieurs pages (combat, écran des résultats, etc.), nous avons opté pour une réutilisation par instanciation au lieu de recharger à chaque fois. L'utilisation de scene.clone() crée des problèmes où les animations se doublent sur toutes les instances due à un désordre dans la structure osseuse.
Pour résoudre ceci, nous avons utilisé SkeletonUtils.clone pour partager uniquement la géométrie tout en dupliquant le squelette de manière indépendante.
Les paramètres d'éclairage et le contrôle de la caméra ont été unifiés dans un seul composant de scène réutilisable pour éviter de les reconfigurer à chaque page. Grâce à la structure React, les composants identiques partagent la mémoire, réduisant ainsi les coûts de rendu redondants.
La structure même du maillage étant en haute définition, l'optimisation à l'exécution avait ses limites, et il restait beaucoup à faire au niveau de Blender. En particulier, appliquer le LOD (Level of Detail) qui réduit automatiquement le nombre de polygones en fonction de la distance à la caméra aurait pu améliorer les performances mobiles. C'était un aspect que je voulais absolument essayer dans le prochain projet.

Les goulets d'étranglement de performance rencontrés avec le premier personnage 'Fundi' m'ont beaucoup appris. Dans un environnement web, la 3D doit être non seulement un « modèle esthétique » mais aussi un « asset soigneusement calculé ». Dans ce projet de personnage humain, nous avons appliqué dès la phase de conception le pipeline d'optimisation suivant.
Dans le passé, nous utilisions des dizaines d'images par pièce, surchargeant ainsi le moteur. Désormais, les parties semblables du visage, du torse, des vêtements, des accessoires et de l'intérieur ont été regroupées pour former trois atlas de taille 2048px chacun.
Dans les flux de travail PBR, il faut des textures séparées pour l’Occlusion, la Rugosité et le Métal. Cependant, trois textures signifient un usage triplé de mémoire GPU et de bande passante réseau.
En capitalisant sur le fait que ces trois types de données sont des images en niveaux de gris avec des valeurs de 0 à 1, nous les avons intégrées dans l'image**: R (AO), G (Rugosité), B (Métallique)** respectivement.

Pour automatiser notre pipeline, au lieu d'utiliser des outils séparés, nous avons conçu un script en utilisant Node.js et la bibliothèque sharp, ce qui s'est révélé plus efficace que prévu.
Avec sharp, vous pouvez traiter plusieurs textures en masse. Surtout dans le cas des cheveux nécessitant un canal alpha, vous pouvez gérer et étendre à ORMA.
Pour les cheveux et manteaux fréquemment modifiés, ils ont été cuits en niveaux de gris, puis injectés directement dans material.color.set() lors de l'exécution Three.js afin d'économiser la mémoire et d'améliorer la flexibilité de personnalisation.
| Niveaux de gris | Mapping couleur |
|---|---|
![]() | ![]() |
Afin d'éviter la désertion des utilisateurs, nous avons réduit la taille des modèles de plus de 80 % et raccourci considérablement le temps de chargement du réseau en exportant sous compression Draco dans Blender et en décompressant dans Three.js.
La conclusion que nous avons tirée de ce travail est que la création de personnages en 3D n'est pas une question de modélisation, mais un problème de conception structurelle adaptée à l'environnement d'exécution.
Une structure qui fonctionne parfaitement dans Blender peut être totalement inutile dans Three.js. À l'inverse, même si c'est un peu contraignant dans Blender, concevoir de manière à ce qu'il soit contrôlable par le moteur améliore nettement le résultat. Grâce aux difficultés rencontrées avec le premier personnage, nous avons pu réaliser le deuxième correctement dès le départ. L'expérience de nous être égarés dans la mauvaise direction nous a finalement permis d'établir de bons critères de conception.
=== 🔍 DEBUG 1 : Vérification de la présence du rig ===
debug1.tsx:45 ✅ hand_ik.L: {type: 'Bone', isBone: true, rotation: 'x:1.64 y:-0.03 z:-1.66'}
debug1.tsx:45 ✅ hand_ik.R: {type: 'Bone', isBone: true, rotation: 'x:1.64 y:0.03 z:1.66'}
debug1.tsx:45 ✅ hips: {type: 'Bone', isBone: true, rotation: 'x:0.00 y:0.00 z:0.00'}
debug1.tsx:45 ✅ torso: {type: 'Bone', isBone: true, rotation: 'x:0.00 y:0.00 z:0.00'}
debug1.tsx:45 ✅ tail: {type: 'SkinnedMesh', isBone: false, rotation: 'x:0.00 y:0.00 z:0.00'}
debug1.tsx:45 ✅ tail001: {type: 'Bone', isBone: true, rotation: 'x:0.69 y:-0.09 z:0.37'}
debug1.tsx:45 ✅ tail002: {type: 'Bone', isBone: true, rotation: 'x:0.75 y:-0.19 z:-0.33'}
debug1.tsx:45 ✅ tail003: {type: 'Bone', isBone: true, rotation: 'x:-0.48 y:-0.27 z:-0.73'}
debug1.tsx:45 ✅ tail004: {type: 'Bone', isBone: true, rotation: 'x:-0.33 y:-0.04 z:-0.06'}
debug1.tsx:45 ✅ ear.L: {type: 'Bone', isBone: true, rotation: 'x:0.00 y:0.00 z:0.00'}
debug1.tsx:45 ✅ ear.R: {type: 'Bone', isBone: true, rotation: 'x:0.00 y:0.00 z:0.00'}
debug1.tsx:45 ✅ head: {type: 'Bone', isBone: true, rotation: 'x:1.41 y:-0.00 z:0.00'}
debug1.tsx:45 ✅ MCH-eye_commonparent: {type: 'Bone', isBone: true, rotation: 'x:-1.57 y:0.00 z:0.00'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:-0.42 y:0.35'}
debug1.tsx:160 🔄 Rotation automatique: {head_y: '-0.054'}
debug1.tsx:160 🔄 Rotation automatique: {head_y: '0.134'}
debug1.tsx:160 🔄 Rotation automatique: {head_y: '0.197'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:-0.04 y:0.19'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:0.62 y:0.74'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:0.65 y:0.75'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:-0.07 y:0.91'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:-0.96 y:0.04'}
debug1.tsx:121 🦊 Agitation de la queue: {wag: '1.00', tail_y: '0.30', tail1_y: '0.25'}
debug1.tsx:105 💃 Basculement des hanches: {sway: '-0.100', hips_rotation_z: '-0.100', hips_position_x: '-0.005'}
debug1.tsx:105 💃 Basculement des hanches: {sway: '0.061', hips_rotation_z: '0.061', hips_position_x: '0.003'}
debug1.tsx:105 💃 Basculement des hanches: {sway: '0.020', hips_rotation_z: '0.020', hips_position_x: '0.001'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:0.51 y:0.55'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:-0.62 y:-0.19'}
debug1.tsx:147 👀 Suivi du regard: {headCommon_x: '-1.571', headCommon_y: '0.000', pointer: 'x:-0.39 y:0.00'}
debug1.tsx:160 🔄 Rotation automatique: {head_y: '-0.084'}
debug1.tsx:160 🔄 Rotation automatique: {head_y: '0.108'}
debug1.tsx:160 🔄 Rotation automatique: {head_y: '0.200'}// Remplacement dynamique du matériau de la sphère oculaire (concept)
// Après le chargement du GLB, trouver le mesh correspondant aux yeux
// Remplacement direct par MeshPhysicalMaterial
// - transmission : 1 (effet de réfraction de la lumière)
// - ior : 1,45 (indice de réfraction du verre)
// - thickness : 0,5 (épaisseur du matériau)
// - roughness : 0 (surface lisse)
// Matériau brillant des yeux
const eyeTextures = useTexture(
enhancedEyes
? [
'/character/textures/eyes_color.png',
'/character/textures/eyes_roughness.png',
'/character/textures/eyes_transmission.png',
]
: [],
);
// Remplacement du matériau des yeux
const eyeMaterial = useFundyEyeMaterial(eyeTextures);
useApplyEyeMaterials({ enhancedEyes, eyeMaterial, nodes, materials });
/**
* Hook pour appliquer le matériau des yeux/iris sur le modèle
*/
export function useApplyEyeMaterials(params: {
enhancedEyes: boolean;
eyeMaterial?: THREE.Material;
nodes: GLTFResult['nodes'];
materials: GLTFResult['materials'];
}) {
const { enhancedEyes, eyeMaterial, nodes, materials } = params;
useEffect(() => {
const eyeMat = enhancedEyes && eyeMaterial ? eyeMaterial : materials.eye;
const irisMat = materials.iris;
const eyeMeshes = [nodes.Sphere001, nodes.Sphere003].filter(
(mesh): mesh is THREE.Mesh => !!mesh && (mesh as THREE.Mesh).isMesh,
);
const irisMeshes = [nodes.Sphere001_1, nodes.Sphere003_1].filter(
(mesh): mesh is THREE.Mesh => !!mesh && (mesh as THREE.Mesh).isMesh,
);
eyeMeshes.forEach(mesh => {
mesh.material = eyeMat;
mesh.material.needsUpdate = true;
});
irisMeshes.forEach(mesh => {
mesh.material = irisMat;
});
}, [enhancedEyes, eyeMaterial, materials.eye, materials.iris, nodes]);
}// useFixSkinnedMesh.ts
export function useFixSkinnedMesh(scene: THREE.Group | THREE.Object3D) {
useEffect(() => {
if (!scene) return;
scene.traverse(child => {
if (child instanceof THREE.SkinnedMesh) {
// frustumCulled = false : inclut les éléments hors écran dans le rendu
child.frustumCulled = false;
// Vérification de la validité de la géométrie et correction manuelle du boundingBox : recalcul de la portée de rendu à la taille réelle
const geo = child.geometry;
if (geo?.attributes?.position && geo.attributes.position.count > 0) {
try {
if (!geo.boundingBox) geo.computeBoundingBox();
if (!geo.boundingSphere) geo.computeBoundingSphere();
} catch (error) {
console.warn(`Failed to compute bounds for ${child.name}:`, error);
}
}
// Initialisation des MorphTargets : remise à l'état initial des valeurs d'expression
if (child.morphTargetInfluences && child.morphTargetDictionary) {
// initialisation si morphTargetInfluences est undefined
const morphCount = Object.keys(child.morphTargetDictionary).length;
if (child.morphTargetInfluences.length !== morphCount) {
child.morphTargetInfluences = new Array(morphCount).fill(0);
}
}
}
});
}, [scene]);
}/**
* FundyModel.tsx
* Implémentation d'instanciation indépendante utilisant SkeletonUtils
*/
export const FundyModel = forwardRef<THREE.Group, FundyModelProps>(
({ animation, enhancedEyes = true, trophyHold = false, ...props }, ref) => {
const group = useRef<THREE.Group>(null!);
const [actionClips, setActionClips] = useState<FundyActionClips>({});
// 1. Chargement GLTF (mise en cache automatique)
const { scene, animations } = useGLTF('/character/model.glb') as unknown as GLTFResult;
// 2. Clonage de la scène : utiliser SkeletonUtils.clone pour des animations indépendantes par instance
const clone = useMemo(() => SkeletonUtils.clone(scene), [scene]);
// 3. Extraction des nodes et des materials de la scène clonée
const { nodes, materials } = useGraph(clone) as unknown as GLTFResult;
// 4. Connexion d'un AnimationMixer indépendant
const { actions, clips, mixer } = useAnimations(animations, group);
// [omission : logique de mappage des clips d'animation]
// 5. Application d'une optimisation hybride
useFixSkinnedMesh(clone); // Crochet de correction des erreurs de rendu
useMorphAnimation(nodes, animation); // Crochet de contrôle des expressions (Morph Target)
return (
<group ref={group} {...props} dispose={null}>
<group name="Scene">
<group name="bone_body">
{/* Rendu de SkinnedMesh et structure Bone */}
<Primitive object={nodes['DEF-spine']} />
<skinnedMesh
name="body"
geometry={nodes.body.geometry}
material={materials.body}
skeleton={nodes.body.skeleton}
/>
{/* ...autres parties */}
</group>
</group>
</group>
);
}
);// Logique centrale du script de composition ORM
const orm = Buffer.alloc(SIZE * SIZE * 3);
for (let i = 0; i < SIZE * SIZE; i++) {
orm[i * 3 + 0] = ao[i]; // Canal R pour AO
orm[i * 3 + 1] = rough[i]; // Canal G pour Rugosité
orm[i * 3 + 2] = metal[i]; // Canal B pour Métal
}
await sharp(orm, { raw: { width: SIZE, height: SIZE, channels: 3 } })
.png({ colours: 256 }) // Compression PNG-8
.toFile(`${name}_ORM.png`);Le récit d'une aventure intense de 7 semaines : de l'amélioration de l'onboarding et du monitoring d'infrastructure à la communication temps réel et l'optimisation de personnages 3D.
Mon expérience durant la phase Challenge de l'OSCCA : propositions d’issues et première Pull Request sur le projet Githru.

Retour sur les 6 semaines de sprint de groupe : défis d'architecture, dynamiques de collaboration et progression technique suite aux retours seniors.

Un compte rendu de l'apparition des modèles d'architecture UI (MVC · MVP · MVVM · Flux), avec leurs avantages et inconvénients, et une analyse comparative.
Enregistrement des problèmes rencontrés lors de l'intégration de personnages dans Three.js pour le projet funda et mon portfolio personnel
Un parcours de 7 mois, des bases au projet de groupe : acquérir les fondamentaux de l'informatique, maîtriser l'ingénierie de l'IA et découvrir l'essence de la conception logicielle.
Le récit d'une aventure intense de 7 semaines : de l'amélioration de l'onboarding et du monitoring d'infrastructure à la communication temps réel et l'optimisation de personnages 3D.
De l'idéation à l'implémentation du MVP en passant par les retours seniors : récit de la mise en place des fondations du projet et des défis techniques relevés
Une analyse de l'essence de la normalisation à travers la logique de mouvement dans les jeux et l'application de l'échelle dans Blender.
Retour sur les 6 semaines de sprint de groupe : défis d'architecture, dynamiques de collaboration et progression technique suite aux retours seniors.
Retour sur dix semaines d’apprentissage au Boostcamp : technologies étudiées, défis de conception, fatigue mentale et enseignements tirés de l’utilisation de l’IA.
Un retour sur la phase Challenge du Boostcamp : missions quotidiennes, apprentissage CS, feedback entre pairs, travail d’équipe et évolution de ma façon d’apprendre avec l’IA.
Exploration des avantages et inconvénients de la programmation fonctionnelle et orientée objet, et des raisons du choix fonctionnel dans React