[#_lesson_23_models_and_light] == Leçon 23 : Modèles et lumière image::learn/23-models.png[La leçon terminée, 480] Les boîtes et les sphères vous mèneront loin, mais tôt ou tard le jeu voudra un dragon. Un *modèle* est une forme que quelqu'un d'autre a construite dans un logiciel de modélisation : des centaines de triangles, ses propres couleurs et surfaces, parfois un squelette et des mouvements qui vont avec, le tout dans un seul fichier. En charger un, c'est deux lignes. Lui donner l'air de quelque chose, c'est de la lumière, et la lumière occupe l'essentiel de cette leçon. Vous n'avez besoin d'aucun graphisme et d'aucun téléchargement. La toute première fois que vous avez lancé Singe, il a extrait un dossier appelé `Singe` dans votre dossier de travail, et deux modèles s'y trouvent : `DragonModel.glb`, le dragon de l'écran titre du moteur lui-même, et `SingeText.glb`, le lettrage en dessous. Cette leçon se sert du dragon. [#_the_scene_around_it] === La scène autour Un modèle a besoin d'un endroit où se tenir. Créez un dossier, créez-y `dragon.singe`, et commencez par un sol et une caméra. Si vous avez fait la leçon vingt-deux, rien de tout ça n'est nouveau ; sinon, c'est une scène allumée, une dalle plate où se tenir, et un point d'où regarder. [source,lua] ---- sceneEnable(true) sceneSetBackground(10, 12, 20) sceneSetAmbient(28, 30, 38) local stone = materialNew() materialSetColor(stone, 105, 105, 115) materialSetRoughness(stone, 0.9) local floor = nodeNew() nodeSetMesh(floor, meshBox(24, 0.4, 24), stone) nodeSetPosition(floor, 0, -0.2, 0) local camera = nodeNew() nodeSetPosition(camera, 0, 2.6, 9) nodeLookAt(camera, 0, 1.7, 0) cameraSet(camera) cameraSetPerspective(55, 0.1, 200) function onOverlayUpdate() return OVERLAY_UPDATED end ---- Lancez-le avec `Singe -R dragon`. Une dalle grise, vue légèrement de haut, sur un fond bleu foncé. La dalle fait vingt-quatre unités de côté et un peu moins d'une demi-unité d'épaisseur, et la poser à `y = -0.2` met sa surface du dessus exactement à `y = 0`, ce qui rend facile de raisonner sur tout ce que vous placerez ensuite : le sol, c'est zéro. [#_loading_the_dragon] === Charger le dragon Deux lignes. [source,lua] ---- local dragonModel = modelLoad("Singe/DragonModel.glb") local dragon = modelInstance(dragonModel) ---- Mettez-les après le sol et enregistrez. Un dragon apparaît, et il est bien trop grand pour l'image -- vous regardez le milieu de son corps. [#_what_just_happened_19] === Ce qui vient de se passer Les deux lignes font des travaux différents, et la différence compte. `modelLoad` lit le fichier, confie les formes et leurs surfaces à la carte graphique, et garde le tout prêt. C'est l'appel lent. Faites-le une fois, quand le jeu démarre, et jamais en pleine partie. `modelInstance` place une copie dans la scène et vous renvoie un nœud -- un nœud ordinaire, du même genre que ce que vous donne `nodeNew`, que vous placez, tournez, mettez à l'échelle et supprimez avec les appels que vous connaissez déjà. Instancier, c'est l'appel bon marché. Chargez un arbre, instanciez-le quarante fois, et vous avez un bois, avec une seule copie des formes sur la carte graphique et quarante endroits où elles sont dessinées. Ce qui vient avec le modèle, c'est tout ce que contenait le fichier : les formes, les surfaces et leurs couleurs, la façon dont les parties sont agencées, l'éventuel squelette et les éventuels mouvements. Vous ne construisez pas de matériaux pour un modèle chargé ; il a apporté les siens. [#_scale_and_the_first_thing_that_goes_wrong] === L'échelle, et le premier problème Le dragon mesure environ sept unités et demie de haut. Votre caméra est à neuf unités en arrière. Voilà pourquoi il remplit la fenêtre. [source,lua] ---- nodeSetScale(dragon, 0.5) ---- Ajoutez ça sous la ligne `modelInstance`. Maintenant il mesure moins de quatre unités, il se tient sur le sol, et il tient dans le cadre. Ce n'est pas un détail ; c'est le premier problème le plus courant avec un modèle, et il a deux visages. Un modèle construit en centimètres arrive cent fois trop grand et vous voyez une seule surface énorme qui remplit l'écran, ce qui se lit en général comme « ma scène est cassée » plutôt que comme « je suis dans le cou du dragon ». Un modèle construit dans une autre unité arrive si petit que c'est un point, ou qu'il est invisible sur le fond, et vous supposez que le fichier ne s'est pas chargé. La solution est toujours la même : une échelle sur le nœud de l'instance. Pas sur les parties, pas dans le logiciel de modélisation si vous pouvez l'éviter, mais un seul `nodeSetScale` sur le nœud que `modelInstance` vous a donné, choisi en essayant un nombre et en regardant. `0.01` et `100` sont des réponses tout à fait ordinaires. Quand vous n'arrivez pas à savoir quelle taille fait une chose, donnez-vous une règle. Mettez une boîte d'une unité à côté : [source,lua] ---- local ruler = nodeNew() nodeSetMesh(ruler, meshBox(1, 1, 1), stone) nodeSetPosition(ruler, 3, 0.5, 0) ---- Un objet de la taille d'une caisse à côté du modèle vous dit d'un coup d'œil si le modèle est un dragon ou une montagne. Retirez la règle quand vous avez fini. Pendant que vous le placez, tournez-le vers vous : [source,lua] ---- nodeSetRotation(dragon, 0, -60, 0) ---- Le dragon a été modélisé tourné vers les X positifs, c'est-à-dire vers la droite, donc il commence de profil. Le tourner autour de Y le ramène vers la caméra. L'« avant » d'un modèle, c'est la direction qu'a choisie la personne qui l'a construit, et pour le savoir il faut le tourner et regarder. [#_light] === La lumière Lancez ce que vous avez maintenant. Le dragon est là, et il est morne : une forme plate et terne, sans reflets et sans ombre. C'est parce que la seule lumière de votre scène est la lumière ambiante, la faible lueur venue de partout que règle `sceneSetAmbient`. C'est la déception la plus courante en 3D, et elle arrive à tout le monde : le modèle s'est chargé parfaitement et la scène est noire, ou presque, parce que rien ne l'éclaire. Avant de soupçonner le fichier, mettez une lumière. Il y en a trois sortes, et chacune est un nœud, donc vous les placez et les orientez avec `nodeSetPosition` et `nodeLookAt` comme n'importe quoi d'autre. [source,lua] ---- local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, -6, 9, 7) nodeLookAt(sun, 0, 1.5, 0) lightSetColor(sun, 255, 244, 224) lightSetIntensity(sun, 1.8) lightSetShadow(sun, true) ---- `LIGHT_DIRECTIONAL` est un soleil. Il est traité comme s'il était infiniment loin, donc seule compte la direction dans laquelle il brille, et tout dans la scène en reçoit la même quantité. Sa position est là pour que `nodeLookAt` puisse l'orienter. C'est la lumière qui donne à une scène sa forme principale, et la plupart des scènes en veulent exactement une. `lightSetIntensity`, c'est la luminosité. Pour une lumière directionnelle, `1` éclaire une surface blanche qui lui fait face jusqu'au blanc, donc `1.8`, c'est une journée lumineuse. `lightSetColor` la teinte : un blanc légèrement chaud pour le soleil, un bleu froid pour le clair de lune, et la section Lumière de la référence pour le reste. `lightSetShadow(sun, true)` est ce qui fait que le dragon est posé sur le sol au lieu de flotter au-dessus. Les ombres sont éteintes par défaut parce que chaque lumière qui projette des ombres coûte une passe de plus sur toute la scène à chaque image. Allumez-les pour la seule lumière qui compte et laissez-les éteintes pour les autres ; une petite machine vous remerciera. [source,lua] ---- local torch = lightNew(LIGHT_POINT) nodeSetPosition(torch, 3.5, 1.2, 3.0) lightSetColor(torch, 255, 150, 70) lightSetIntensity(torch, 14) lightSetRange(torch, 12) ---- `LIGHT_POINT` est une ampoule. Elle se trouve à un endroit, brille dans toutes les directions, et faiblit avec la distance. C'est à cause de cet affaiblissement que le nombre est `14` et pas `1` : l'intensité d'une lumière ponctuelle est sa luminosité à l'endroit même de la lumière, et une fois qu'elle a traversé quelques unités, il n'en reste plus grand-chose. Une ampoule qui éclaire une pièce demande en général quelque chose dans les dizaines. Si une lumière ponctuelle semble ne rien faire, c'est presque toujours pour cette raison. `lightSetRange` dit jusqu'où elle porte avant de s'arrêter complètement, ce qui empêche une lampe d'une pièce d'éclairer faiblement la suivante, et ça ne coûte rien à régler. La troisième sorte est `LIGHT_SPOT`, un cône pointé le long du Z négatif du nœud lui-même, avec `lightSetCone` pour l'angle du milieu brillant et du bord adouci : une lampe torche, un phare, un projecteur. Jusqu'à huit lumières brillent à la fois, ce qui est plus qu'il n'en faut à la plupart des scènes. Si vous en voulez plus, c'est que vous en voulez moins. Lancez-le maintenant. Un soleil presque blanc venu de la gauche, une lueur orange et chaude venue de la droite, une ombre sur la dalle, et un dragon qui a un devant et un derrière. [#_materials_at_the_level_you_need_today] === Les matériaux, juste ce qu'il vous faut aujourd'hui Le dragon a apporté ses propres matériaux, et il y en a trois : un métal sombre, un métal plus pâle couleur champagne, et un verre pâle dans lequel on voit un peu en profondeur. La plupart de ses parties sont taillées dans plus d'un. Vous n'avez à toucher à rien de tout ça, et la plupart du temps, vous ne devriez pas. Quand vous voulez vraiment changer l'aspect d'une partie d'un modèle, vous créez un matériau comme dans la leçon vingt-deux et vous le mettez sur la partie : [source,lua] ---- local red = materialNew() materialSetColor(red, 220, 50, 40) materialSetRoughness(red, 0.4) nodeSetMaterial(nodeFind("crest", dragon), red) ---- `nodeFind(name, root)` cherche dans une instance de modèle une partie d'après le nom qu'elle avait dans le fichier et renvoie son nœud, ou `nil` quand ce nom n'existe pas. C'est comme ça qu'on atteint l'intérieur d'un modèle : une tourelle, une roue, une main où accrocher une torche, ou la crête du dragon. La crête est d'une seule pièce et d'un seul matériau, donc son nœud porte la forme elle-même et la ligne ci-dessus la repeint. Une partie faite de plusieurs matériaux est gardée sous forme d'un nœud avec un enfant par pièce, et mettre un matériau sur le parent ne change rien de visible. Quand un `nodeSetMaterial` semble ne rien faire, c'est pour ça, et `nodeGetChildren` vous montrera ce qu'il y a en dessous. Quatre nombres décrivent la plupart des surfaces. La couleur, avec `materialSetColor`. `materialSetRoughness`, de `0` pour un miroir à `1` pour de la craie. `materialSetMetallic`, qui vaut `0` pour presque tout -- la peinture, le bois, le plastique, la peau -- et `1` pour du vrai métal, les valeurs intermédiaires servant peu. Et `materialSetEmissive`, qui fait briller une surface de sa propre couleur pour les écrans, les lampes et le fer chauffé au rouge, sans rien éclairer autour. Une chose à savoir sur le métal : une surface métallique vous montre ses reflets plutôt qu'une couleur à elle, donc dans une scène où il n'y a rien à refléter, elle paraît sombre et terne. C'est pour ça que le métal sombre du dragon a besoin du soleil pour prendre vie. La vraie solution est un ciel (`sceneSetSky`), qui enveloppe la scène dans un panorama qui à la fois s'affiche derrière tout et l'éclaire ; la section « L'aspect de l'image » de la référence en parle. [#_movement_the_model_brings_with_it] === Les mouvements que le modèle apporte Beaucoup de modèles transportent des animations : une marche, une course, une attente, une porte qui s'ouvre. Elles sont stockées dans le fichier, et en jouer une se fait en un seul appel sur le nœud racine de l'instance. [source,lua] ---- animationPlay(dragon, "Walk", true) ---- Le deuxième argument est le nom du clip tel que le fichier le stocke, ou son numéro. `true` veut dire le jouer en boucle pour toujours. Deux nombres facultatifs de plus règlent la vitesse et le fondu enchaîné : `animationPlay(hero, "Run", true, 1.2, 0.4)` joue la course un cinquième plus vite et passe en fondu vers elle en quatre dixièmes de seconde, si bien qu'une marche devient une course sans à-coup. `animationStop`, `animationPause` et `animationIsPlaying` font ce que dit leur nom. Vous ne pouvez pas encore lancer cette ligne, et voici la raison honnête : le dragon de votre dossier `Singe` ne contient aucune animation. C'est un modèle, pas une performance d'acteur. Demander un clip qui n'existe pas arrête le jeu avec une erreur, donc ne supposez jamais -- demandez : [source,lua] ---- local clips = modelGetAnimations(dragonModel) if #clips > 0 then animationPlay(dragon, 1, true) else debugPrint("DragonModel.glb carries no animation clips. The wings are turned by hand.") end ---- `modelGetAnimations` renvoie une table des noms de clips du fichier, dans l'ordre, donc `clips[1]` est le premier et `#clips` est leur nombre -- les appels de table de la leçon six, sur une table que le moteur a remplie. Lancez-le et regardez la console : le message apparaît, parce que ce nombre vaut zéro. Prenez l'habitude de cette vérification avec tout modèle que vous n'avez pas fait vous-même. Elle coûte trois lignes et elle transforme « le jeu est mort au démarrage » en une phrase dans le journal. [#_moving_the_parts_yourself] === Bouger les parties vous-même Un modèle sans animations n'est pas une statue. Ses parties sont des nœuds, et les nœuds tournent. [source,lua] ---- local wingLeft = nodeFind("wingL", dragon) local wingRight = nodeFind("wingR", dragon) ---- [source,lua] ---- function onOverlayUpdate() local flap = math.sin(singeGetTicks() / 260) * 22 nodeSetRotation(wingLeft, flap, 0, 0) nodeSetRotation(wingRight, -flap, 0, 0) nodeRotate(dragon, 0, 0.25, 0) overlayClear() overlayPrint(2, 2, "Singe/DragonModel.glb") overlayPrint(2, 4, "animation clips in the file: " .. #clips) return OVERLAY_UPDATED end ---- Enregistrez. Les ailes battent, le dragon tourne lentement sur place, et l'ombre sur le sol bat avec elles. `singeGetTicks` est le nombre de millisecondes depuis le démarrage du moteur, donc il grimpe régulièrement, sans fin. `math.sin` prend ce nombre qui grimpe et renvoie une valeur qui glisse en douceur de `-1` à `1` et retour, un battement complet environ toutes les secondes et demie avec ce diviseur. Multiplier par `22` en fait un angle de vingt-deux degrés de chaque côté. Diviser par un nombre plus grand ralentit le battement ; multiplier par un nombre plus grand l'élargit. Cette seule ligne est l'animation la moins chère qui existe, et elle vaut la peine d'être gardée sous le coude. Les ailes tournent autour de leur propre axe X, et elles tournent autour de l'épaule plutôt qu'autour du milieu de l'animal, parce que la personne qui a construit le fichier a mis le nœud de chaque partie là où se trouve l'articulation. Un modèle qui n'a pas été construit ainsi fera tourner ses parties autour du centre du modèle, et la solution est de déplacer le nœud de la partie, ce dont parle la section Nœud de la référence. Remarquez que le dragon entier qui tourne ne gêne pas les ailes qui tournent. Les parties sont accrochées sous la racine de l'instance, donc tourner la racine emporte tout avec elle, et la rotation propre de chaque aile s'ajoute par-dessus. Cette disposition -- une chose dans une chose dans une chose, chacune bougeant dans le monde de son parent -- est ce à quoi sert un arbre de scène. [#_your_own_models] === Vos propres modèles Quand vous faites les vôtres, exportez depuis Blender -- ou Maya, ou 3ds Max, ou n'importe quel autre logiciel moderne -- en *glTF 2.0 binaire*, c'est-à-dire un fichier `.glb`. C'est le choix par défaut de Blender, et c'est le seul format de modèle que Singe lit. La seule règle est que le fichier doit se suffire à lui-même : les formes, les images posées dessus et les mouvements, tous à l'intérieur de l'unique `.glb`. Un fichier qui renvoie à un `.bin` ou à un `.png` posé à côté de lui est refusé, avec un message qui nomme ce qu'il voulait. L'exportateur de Blender fait ce qu'il faut avec ses réglages par défaut. Tout ce que vous pouvez construire ne survit pas au voyage. La section « Modèles et animation » de la Référence, dans le chapitre « Scènes 3D », en donne la liste : ce qui est lu, ce qui est ignoré sans bruit, et les limites (un squelette peut avoir jusqu'à cent vingt-huit articulations, par exemple). Lisez-la une fois avant de passer un week-end sur un modèle, pas après. [#_try_it_23] === Essayez . *Éteignez les lumières.* Mettez tout le bloc `sun` en commentaire en ajoutant `--` au début de chaque ligne, et lancez. Puis faites de même avec `sceneSetAmbient`. Retenez à quoi ça ressemble, parce qu'un jour ça vous arrivera sans que vous l'ayez voulu. . *Déplacez le soleil.* Changez `nodeLookAt(sun, 0, 1.5, 0)` en `nodeLookAt(sun, 0, 20, 0)` pour que le soleil vise au-dessus de la tête du dragon. Regardez l'ombre et les reflets s'en aller. Orienter une lumière n'est pas de la décoration. . *Réchauffez la torche.* Montez l'intensité de la lumière ponctuelle de `14` à `60` et baissez sa portée à `4`. Puis mettez-la en `(0, 6, 0)`, au-dessus du dragon. Une lumière ponctuelle est une ampoule, et c'est vous qui choisissez où l'ampoule est suspendue. . *Instanciez-le deux fois.* Appelez `modelInstance(dragonModel)` une deuxième fois, gardez le résultat dans une autre variable, mettez-le à l'échelle, et placez-le en `(-4, 0, -3)`. Un chargement, deux dragons. Puis essayez plutôt de charger le modèle deux fois et comprenez pourquoi il ne faut pas. . *Trouvez une autre partie.* Les parties du dragon s'appellent `head`, `jaw`, `neck`, `tail`, `body`, `crest`, `hornL`, `hornR`, `wingL`, `wingR`, `foreLegL`, `foreLegR`, `hindLegL` et `hindLegR`. Faites onduler la queue avec son propre `math.sin` à une autre vitesse que les ailes. Puis essayez `nodeFind("wings", dragon)` et voyez ce que fait un mauvais nom quand vous passez directement le `nil` à `nodeSetRotation`. [#_break_it_on_purpose_22] === Cassez-le exprès Changez le nom du fichier en `Singe/Dragonmodel.glb` -- une seule majuscule -- et lancez. Singe s'arrête avant que le jeu démarre : ---- 13:modelLoad: Unable to read Singe/Dragonmodel.glb. ---- La ligne, l'appel et la plainte, dans cet ordre. « Unable to read » veut dire que le moteur a cherché et n'a rien trouvé, ce qui est presque toujours l'une de trois choses : le nom est mal orthographié, les majuscules sont fausses, ou le fichier n'est pas là où vous l'avez dit. Sous Windows, les majuscules vous auraient été pardonnées, et votre jeu aurait ensuite échoué pour tout le monde sous Linux, ce qui est un bug pire que celui-ci. Tapez les noms exactement. Le même message avec un vrai nom de fichier veut dire que le fichier n'est pas là où le moteur a regardé. Le dossier `Singe` est créé dans le dossier *depuis* lequel vous lancez le moteur, donc si vous avez démarré Singe depuis ailleurs, `Singe/DragonModel.glb` pointe ailleurs aussi. [#_what_you_learned_23] === Ce que vous avez appris * `modelLoad` lit un `.glb` une fois ; `modelInstance` en place des copies aussi souvent que vous voulez. * La racine d'une instance est un nœud ordinaire : placez-la, tournez-la, mettez-la à l'échelle et supprimez-la comme n'importe quel autre. * Un modèle apporte avec lui ses propres formes et ses propres matériaux. * Un modèle arrive à la taille qu'a utilisée son auteur, et c'est pour ça qu'un `nodeSetScale` sur l'instance fait partie de sa mise en place. * Une scène sans lumière est noire ou presque, et c'est la première chose à vérifier quand un modèle a l'air faux. * Une lumière directionnelle est un soleil, une lumière ponctuelle est une ampoule qui faiblit avec la distance et demande une intensité bien plus grande, et un spot est un cône. * Les ombres sont éteintes tant que `lightSetShadow` ne les allume pas, et chacune a un coût. * `nodeFind` atteint une partie d'un modèle par le nom qu'elle a dans le fichier. * `modelGetAnimations` dit ce qu'un modèle peut jouer, et demander un clip qui n'existe pas est une erreur. * Un modèle sans animations peut quand même être bougé une partie à la fois. [#_next_time_23] === La prochaine fois Tout dans cette scène bouge parce que vous le lui avez dit, image par image. La prochaine leçon confie ce travail à un moteur physique : des caisses qui tombent, roulent, s'empilent et se renversent les unes les autres sans que vous écriviez une ligne d'arithmétique -- et la question plus difficile de savoir quand il faut le laisser faire. C'est la leçon vingt-quatre.