[#_lesson_22_into_3d] == Leçon 22 : Passer à la 3D image::learn/22-3d.png[La leçon terminée, 480] Tout ce que vous avez dessiné jusqu'ici était plat. Les sprites, le texte, les boîtes et les lignes vivent tous sur l'overlay, qui est une plaque de verre à l'avant de la fenêtre, avec les mêmes coordonnées qu'à la leçon un. Cette leçon met un monde derrière ce verre : un endroit qui a de la profondeur, où une chose peut être plus près ou plus loin, et où une caméra décide de ce que vous en voyez. À la fin, vous aurez une boîte qui flotte dans l'espace et que vous pouvez tourner avec les touches fléchées, avec les nombres de ses angles affichés dans le coin par le même `overlayPrint` que celui de votre tout premier jour. Ce dernier point compte plus qu'il n'en a l'air, et une section lui est consacrée. [#_a_scene_a_box_and_a_light] === Une scène, une boîte et une lumière Créez un dossier, mettez-y un fichier appelé `spin.singe`, et tapez ceci. [source,lua] ---- sceneEnable(true) sceneSetBackground(18, 20, 34) sceneSetAmbient(35, 35, 45) local paint = materialNew() materialSetColor(paint, 215, 95, 60) materialSetRoughness(paint, 0.6) local box = nodeNew() nodeSetMesh(box, meshBox(2, 2, 2), paint) local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, 4, 6, 5) nodeLookAt(sun, 0, 0, 0) function onOverlayUpdate() nodeRotate(box, 0, 0.5, 0) return OVERLAY_UPDATED end ---- Lancez-le avec `Singe -R spin`. Une boîte orange tourne lentement sur un fond bleu foncé, éclairée d'en haut et de la droite, avec une face claire et les autres qui s'enfoncent dans l'ombre. Il n'y a pas de caméra dans ce script. Singe vous a placé en `(0, 0, 5)`, à regarder le milieu du monde, parce qu'une scène sans caméra doit bien être vue de quelque part. Vous allez remplacer ça par votre propre caméra dans un instant. Si la fenêtre reste noire, lisez la console. Sur une machine dont la carte graphique ne sait pas dessiner une scène 3D, `sceneEnable` arrête le jeu avec un message qui le dit, et il n'y a rien d'autre à faire que de suivre la leçon ailleurs. Les jeux 2D continuent de marcher sur une telle machine ; la 3D, non. [#_what_just_happened_18] === Ce qui vient de se passer Les trois appels du haut mettent en place le monde dans son ensemble. [source,lua] ---- sceneEnable(true) sceneSetBackground(18, 20, 34) sceneSetAmbient(35, 35, 45) ---- `sceneEnable(true)` allume la couche 3D. C'est le seul appel que fait chaque jeu en 3D, et tant que vous ne l'avez pas fait, rien de ce que vous construisez dans la scène n'est dessiné. `sceneSetBackground` est la couleur avec laquelle la scène est effacée à chaque image, rouge, vert et bleu de `0` à `255`, comme les couleurs marchent depuis la leçon neuf. Il y a un quatrième nombre que vous pouvez lui donner pour la transparence, et c'est ainsi qu'un jeu qui lit de la vidéo met un objet 3D devant le film. Laissez-le de côté et le fond est opaque. `sceneSetAmbient` est un peu de lumière qui vient de toutes les directions à la fois. Son travail est d'empêcher les côtés qu'aucune lampe n'atteint d'être complètement noirs. Baissez-la plus tard à `0, 0, 0` et voyez à quel point l'image devient dure. [source,lua] ---- local paint = materialNew() materialSetColor(paint, 215, 95, 60) materialSetRoughness(paint, 0.6) ---- Un *matériau*, c'est l'aspect d'une surface : sa couleur, à quel point elle brille, si elle porte une image. `materialNew` en crée un et renvoie un nombre qui le représente, le même genre de handle que celui que `spriteLoad` vous a donné à la leçon neuf. Gardez-le dans une variable, parce que tous les appels suivants en ont besoin. `materialSetRoughness` va de `0`, un miroir avec un petit reflet dur, à `1`, une surface mate comme de la craie sans aucun reflet. `0.6`, c'est de la peinture sur du bois. Il existe une douzaine d'autres appels `materialSet`, et la prochaine leçon en utilise quelques-uns ; la section Matériau de la référence les liste tous. [source,lua] ---- local box = nodeNew() nodeSetMesh(box, meshBox(2, 2, 2), paint) ---- C'est le plus important. Un *nœud* est un endroit dans le monde. Il a une position, une rotation et une échelle, et à lui seul il ne dessine rien du tout : c'est un point auquel on peut attacher des choses. Tout dans une scène 3D est un nœud, y compris les lumières et la caméra. `nodeNew` en crée un vide au milieu du monde. `nodeSetMesh` y accroche deux choses : un *maillage*, qui est la forme, et le matériau, qui est l'aspect de cette forme. `meshBox(2, 2, 2)` construit une boîte de deux unités de large, deux de haut et deux de profondeur, et renvoie un handle vers elle, comme `materialNew`. Les maillages et les matériaux gagnent à être partagés. Un seul handle de `meshBox` peut servir à cent nœuds, et c'est comme ça qu'on dessine cent caisses à peu de frais. Fabriquer cent boîtes séparées qui se trouvent avoir la même taille, c'est la façon débutante de rendre une scène lente. [source,lua] ---- local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, 4, 6, 5) nodeLookAt(sun, 0, 0, 0) ---- `lightNew` crée aussi un nœud -- un nœud qui porte une lumière -- et renvoie son handle de nœud, si bien que vous le placez et l'orientez avec les mêmes appels que pour tout le reste. `LIGHT_DIRECTIONAL` est un soleil : il est si loin que seule compte la direction dans laquelle il brille, et c'est pour ça que la position ne compte ici que parce que `nodeLookAt` s'en sert pour calculer la direction. Retirez ces trois lignes et relancez. La boîte devient presque noire, parce que la seule lumière qui reste est la faible lumière ambiante. La lumière, c'est toute la leçon vingt-trois, et voici un premier avant-goût de la déception la plus courante en 3D : le modèle s'est bien chargé, et la scène est noire parce que rien ne l'éclaire. [source,lua] ---- nodeRotate(box, 0, 0.5, 0) ---- `nodeRotate` tourne un nœud d'un certain nombre de degrés autour de chacun de ses trois axes à lui, en plus de la rotation qu'il avait déjà. Un demi-degré par image, à une soixantaine d'images par seconde, ça fait un tour toutes les douze secondes. C'est le même truc qu'à la leçon deux : un petit changement, appliqué à chaque image, se voit comme un mouvement. [#_which_way_is_which] === Quel axe va où Trois nombres décrivent un endroit dans la scène, et vous devez savoir ce que chacun veut dire avant de pouvoir mettre quoi que ce soit où que ce soit. * *X* va vers la droite. Un X plus grand est plus à droite. * *Y* va vers le haut. Un Y plus grand est plus haut. Le sol d'une scène est en général à `y = 0`. * *Z* vient vers vous, en sortant de l'écran. Un Z plus grand est plus près de la caméra par défaut, et *le Z négatif est la direction dans laquelle regarde la caméra*. Ce dernier point piège tout le monde. Ce que vous voulez devant la caméra va en Z négatif. Ce n'est pas une bizarrerie de Singe : glTF et Blender utilisent la même disposition, donc un modèle exporté depuis Blender arrive tourné dans le sens qu'attend Singe. Ne me croyez pas sur parole. Ajoutez cette ligne après la ligne `nodeSetMesh`, enregistrez, et regardez : [source,lua] ---- nodeSetPosition(box, 3, 1, 0) ---- La boîte saute vers la droite et vers le haut. `nodeSetPosition` place un nœud à un endroit exact, et remplace la position qu'avait le nœud. Il existe un `nodeMove(node, dx, dy, dz)` qui va avec et qui déplace plutôt un nœud d'une certaine quantité, le long de ses propres axes, si bien que `nodeMove(ship, 0, 0, -0.1)` fait avancer un vaisseau dans la direction où il se trouve pointé, quelle qu'elle soit. Essayez ensuite `nodeSetPosition(box, 0, 0, -20)`. La boîte rapetisse au loin : c'est le Z négatif qui fait son travail. Puis essayez `(0, 0, 20)` : la boîte disparaît, parce que vous l'avez mise derrière la caméra. Deux appels de plus complètent la série. [source,lua] ---- nodeSetRotation(box, 0, 45, 0) nodeSetScale(box, 0.5) ---- `nodeSetRotation` règle directement la rotation en degrés, là où `nodeRotate` y ajoute. Les trois nombres sont autour de X, autour de Y et autour de Z : tourner autour de Y, c'est ce que fait une personne debout sur le sol quand elle se tourne pour regarder ailleurs, et c'est la rotation dont vous vous servirez le plus. `nodeSetScale` rend le nœud plus grand ou plus petit, soit avec un seul nombre pour les trois axes, soit avec trois, un pour chacun. Retirez ces deux expériences avant de continuer. La suite de la leçon part du script tel que vous l'avez tapé au début. [#_your_own_camera] === Votre propre caméra La vue par défaut est une politesse, pas une fonctionnalité. Mettez une caméra. [source,lua] ---- local camera = nodeNew() nodeSetPosition(camera, 0, 2, 7) nodeLookAt(camera, 0, 0, 0) cameraSet(camera) cameraSetPerspective(60, 0.1, 100) ---- Mettez ça sous la lumière, avant votre `onOverlayUpdate`, et lancez. Vous regardez maintenant la boîte légèrement de haut, depuis deux unités plus haut et sept unités en arrière. Une caméra est un nœud comme les autres. `cameraSet` indique au moteur depuis quel nœud regarder, et la scène est dessinée depuis la position de ce nœud, en regardant le long de son propre Z négatif. Orienter une caméra à la main avec `nodeSetRotation` serait une torture, alors `nodeLookAt` le fait pour vous : donnez-lui un point du monde et elle tourne le nœud pour lui faire face. `cameraSetPerspective`, c'est l'objectif. Le premier nombre est le champ de vision en degrés, de haut en bas : la quantité de monde qui est tassée dans la hauteur de la fenêtre. Un petit nombre comme `35` donne un téléobjectif qui aplatit tout ; un grand comme `90` donne un grand-angle qui fait paraître la pièce immense et bomber les coins. `60` est une valeur par défaut confortable. Les deux autres nombres sont la distance la plus proche et la plus lointaine qui sont dessinées, et ce qui se trouve hors de cette plage n'est pas dessiné du tout. Comme la caméra est un nœud ordinaire, tout ce que vous avez appris sur les nœuds marche avec elle. Plus tard, vous accrocherez une caméra sous une voiture qui roule et vous aurez une caméra de poursuite pour rien. [#_turning_it_with_the_arrow_keys] === La tourner avec les touches fléchées Lire le système de coordonnées sur une page est une mauvaise façon de l'apprendre. Y déplacer quelque chose en est une bonne. Remplacez votre `onOverlayUpdate` par ceci, et ajoutez les deux nouveaux callbacks et les quatre variables au-dessus. [source,lua] ---- local turnLeft = false local turnRight = false local tiltUp = false local tiltDown = false ---- Celles-ci vont tout en haut du fichier, au-dessus de tout le reste. [source,lua] ---- function onInputPressed(what) if what == SWITCH_LEFT then turnLeft = true elseif what == SWITCH_RIGHT then turnRight = true elseif what == SWITCH_UP then tiltUp = true elseif what == SWITCH_DOWN then tiltDown = true end end function onInputReleased(what) if what == SWITCH_LEFT then turnLeft = false elseif what == SWITCH_RIGHT then turnRight = false elseif what == SWITCH_UP then tiltUp = false elseif what == SWITCH_DOWN then tiltDown = false end end function onOverlayUpdate() if turnLeft then nodeRotate(box, 0, -1.5, 0) end if turnRight then nodeRotate(box, 0, 1.5, 0) end if tiltUp then nodeRotate(box, -1.5, 0, 0) end if tiltDown then nodeRotate(box, 1.5, 0, 0) end return OVERLAY_UPDATED end ---- `onInputPressed` et `onInputReleased` sont des callbacks, comme `onOverlayUpdate` : vous les écrivez, et Singe les appelle quand une commande s'enfonce ou remonte. Un appui arrive une fois, pas en boucle tant que la touche est tenue, et c'est pour ça que chacun règle une variable et que le callback de dessin fait le travail. Ce schéma -- l'appui lève un drapeau, l'image lit le drapeau -- est la façon d'obtenir « tant que la touche est tenue » d'un moteur qui ne vous parle que des changements. Les noms en `SWITCH_` viennent du moteur lui-même, donc il n'y a rien à charger pour s'en servir. Ce ne sont pas non plus des touches. Ils sont ce qu'une commande *veut dire*, et `controls.cfg` décide quelle touche, quel bouton ou quelle direction de stick produit chacun d'eux. Aujourd'hui, vos touches fléchées envoient `SWITCH_LEFT` et compagnie ; le stick d'une manette envoie la même chose, et votre script ne voit jamais la différence. Les signes sont choisis pour que chaque flèche déplace la face que vous regardez dans la direction où vous avez appuyé. Tourner autour de Y d'un angle positif fait pivoter cette face proche vers la droite, donc la flèche gauche demande un angle négatif. Si vous trouvez ça à l'envers, inversez les signes ; ce sont vos commandes, pas celles du moteur. [#_the_overlay_is_still_on_top] === L'overlay est toujours au-dessus Ajoutez ces lignes à `onOverlayUpdate`, au-dessus du `return`. [source,lua] ---- local pitch, yaw = nodeGetRotation(box) overlayClear() overlayPrint(2, 2, "Arrow keys turn the box.") overlayPrint(2, 4, "yaw " .. math.floor(yaw) .. " pitch " .. math.floor(pitch)) ---- Rien là-dedans n'est nouveau, sauf ce par-dessus quoi c'est affiché. `overlayPrint` est l'appel de la leçon un, qui compte en cellules de caractère et n'a besoin d'aucune police chargée, et il dessine par-dessus une scène 3D sans qu'on lui en ait rien dit. C'est ainsi que les choses sont disposées, et ça mérite d'être dit clairement : Singe dessine d'abord la vidéo, puis la scène 3D par-dessus, puis l'overlay par-dessus le tout. Votre score, vos vies, votre minuterie, votre réticule et vos sous-titres relèvent tous de l'overlay, exactement comme dans la deuxième partie. Un jeu en 3D garde son score dans le coin avec les mêmes cinq lignes qu'un jeu en 2D. `nodeGetRotation` renvoie trois nombres, les angles autour de X, de Y et de Z, et cette ligne garde les deux premiers dans `pitch` et `yaw`. Ce sont leurs noms habituels en anglais : pitch, le tangage, c'est le nez qui monte et qui descend ; yaw, le lacet, c'est tourner à gauche et à droite. `math.floor` jette la partie décimale pour que les nombres arrêtent de clignoter. Regardez le yaw pendant que vous tenez la flèche gauche. Il descend au-delà de `-180` et revient par `180`, parce que les angles sont donnés comme le plus petit tour qui mène là et non comme un total cumulé. Si votre jeu a besoin de savoir combien de fois le joueur a tourné sur lui-même, comptez-le vous-même. [#_try_it_22] === Essayez . *Déplacez la caméra.* Changez `nodeSetPosition(camera, 0, 2, 7)` en `(0, 8, 7)`, puis en `(7, 2, 7)`, puis en `(0, 0.2, 7)`. À chaque fois, le `nodeLookAt` garde la boîte au centre, si bien que vous obtenez un autre angle sur la même scène pour un seul nombre. . *Changez l'objectif.* Essayez `cameraSetPerspective(30, 0.1, 100)`, puis `(90, 0.1, 100)`, sans bouger la caméra. Un jeu paraît rapide avec un champ de vision large et majestueux avec un champ étroit, et c'est le réglage d'ambiance le moins cher qui soit. . *Ajoutez une deuxième boîte.* Copiez les deux lignes qui ont créé la boîte, renommez la variable en `mark`, et donnez-lui `nodeSetPosition(mark, 3, 0, 0)` et `nodeSetScale(mark, 0.3)`. Vous avez maintenant un repère pour juger la première boîte. Mettez-la tour à tour en `(0, 3, 0)` et en `(0, 0, -3)` et dites à voix haute quel axe est lequel. . *Partagez le maillage.* Au lieu d'appeler `meshBox` deux fois, gardez le premier dans une variable et donnez le même handle aux deux nœuds. Rien n'a l'air différent. C'est tout l'intérêt : une forme, deux endroits. . *Accrochez la caméra à la boîte.* Gardez la deuxième boîte de l'étape précédente, sinon il ne restera rien par rapport à quoi bouger. Puis cherchez `nodeSetParent` dans la référence et accrochez la caméra sous la première boîte. Tenez une touche fléchée : la caméra suit la rotation de la boîte, donc la boîte reste immobile et le reste de la scène défile. Vous êtes à une ligne d'une vue à la première personne, et c'est aussi comme ça que vous découvrez qu'une caméra de poursuite demande un parent plutôt que de l'arithmétique. [#_break_it_on_purpose_21] === Cassez-le exprès Faites une faute de frappe dans le nom du matériau. Changez la ligne `nodeSetMesh` pour qu'elle utilise `pain` au lieu de `paint` : [source,lua] ---- nodeSetMesh(box, meshBox(2, 2, 2), pain) ---- Singe s'arrête avant que le jeu démarre et affiche quelque chose comme : ---- 15:nodeSetMesh: Argument 3 must be a number. ---- C'est une erreur d'une autre forme que celle de la leçon un, et elle vient du moteur plutôt que de Lua. Le nombre au début, c'est la ligne. Le nom qui suit, c'est l'appel qui s'est plaint. La plainte elle-même, c'est que la troisième chose que vous lui avez passée n'était pas un nombre. La raison mérite d'être suivie, parce qu'elle explique toute une famille d'erreurs. `pain` est une variable à laquelle on n'a jamais donné de valeur, et en Lua une variable vide vaut `nil` -- la valeur qui veut dire « rien ici ». Passer `nil` à un appel qui attend un handle de matériau donne exactement ça. Quand vous voyez « must be a number » venant d'un appel du moteur, cherchez un nom de variable mal orthographié sur cette ligne, ou un handle que vous avez oublié de garder. [#_what_you_learned_22] === Ce que vous avez appris * `sceneEnable(true)` allume la couche 3D, et rien n'est dessiné en 3D tant que vous ne l'avez pas appelé. * Un nœud est un endroit dans le monde, avec une position, une rotation et une échelle. Les maillages, les lumières et les caméras s'accrochent tous à des nœuds. * Un maillage est la forme et un matériau est l'aspect, et chacun peut être partagé par autant de nœuds que vous voulez. * X va à droite, Y va en haut, et le Z négatif est la direction où regarde la caméra. * `nodeSetPosition` et `nodeSetRotation` règlent une valeur directement ; `nodeMove` et `nodeRotate` la changent d'une certaine quantité. * N'importe quel nœud peut être la caméra. `nodeLookAt` l'oriente et `cameraSet` la choisit. * Le champ de vision de `cameraSetPerspective` change la sensation que donne la scène sans rien déplacer. * Une scène sans lumière est noire, et c'est la première chose à vérifier quand rien n'apparaît. * L'overlay est toujours là, toujours au-dessus, et c'est toujours là que va votre score. [#_next_time_22] === La prochaine fois Une boîte fait un piètre dragon. La prochaine leçon charge un vrai modèle -- un modèle que le moteur a déjà extrait dans votre dossier de travail, donc il n'y a rien à télécharger ni rien à dessiner -- le pose sur le sol, et l'éclaire correctement. C'est la leçon vingt-trois.