singe/docs/fr/lessons/23-models.adoc

471 lines
21 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_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.