singe/docs/fr/lessons/22-3d.adoc

463 lines
19 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[#_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.