[#_lesson_23_models_and_light] == Lektion 23: Modelle und Licht image::learn/23-models.png[Die fertige Lektion, 480] Mit Kisten und Kugeln kommst du weit, aber früher oder später will das Spiel einen Drachen. Ein *Modell* ist eine Form, die jemand anders in einem Modellierprogramm gebaut hat: Hunderte von Dreiecken, eigene Farben und Oberflächen, manchmal ein Skelett und Bewegungen dazu, alles in einer Datei. Eins zu laden sind zwei Zeilen. Dass es nach etwas aussieht, ist eine Frage des Lichts, und Licht ist der größte Teil dieser Lektion. Du brauchst keine Grafiken und keine Downloads. Als du Singe zum allerersten Mal gestartet hast, hat es einen Ordner namens `Singe` in deinen Arbeitsordner entpackt, und darin liegen zwei Modelle: `DragonModel.glb`, der Drache aus dem eigenen Titelbildschirm der Engine, und `SingeText.glb`, der Schriftzug darunter. Diese Lektion benutzt den Drachen. [#_the_scene_around_it] === Die Szene drumherum Ein Modell braucht einen Platz zum Stehen. Leg einen Ordner an, leg darin `dragon.singe` an und fang mit einem Boden und einer Kamera an. Wenn du Lektion zweiundzwanzig gemacht hast, ist nichts davon neu; wenn nicht, ist es eine eingeschaltete Szene, eine flache Platte zum Draufstehen und ein Punkt, von dem aus man schaut. [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 ---- Starte es mit `Singe -R dragon`. Eine graue Platte, leicht von oben gesehen, vor einem dunkelblauen Hintergrund. Die Platte ist vierundzwanzig Einheiten breit und etwas weniger als eine halbe Einheit dick, und wenn du sie auf `y = -0.2` setzt, liegt ihre Oberseite genau bei `y = 0`, und damit ist alles, was du später platzierst, leicht zu durchschauen: Der Boden ist null. [#_loading_the_dragon] === Den Drachen laden Zwei Zeilen. [source,lua] ---- local dragonModel = modelLoad("Singe/DragonModel.glb") local dragon = modelInstance(dragonModel) ---- Setz sie hinter den Boden und speichere. Ein Drache erscheint, und er ist viel zu groß für das Bild -- du schaust auf seine Mitte. [#_what_just_happened_19] === Was gerade passiert ist Die beiden Zeilen haben verschiedene Aufgaben, und der Unterschied ist wichtig. `modelLoad` liest die Datei, gibt die Formen und ihre Oberflächen an die Grafikkarte und hält das Ganze bereit. Das ist der langsame Aufruf. Mach ihn einmal, wenn das Spiel startet, und nie mitten im Spiel. `modelInstance` stellt eine Kopie in die Szene und gibt dir einen Knoten zurück -- einen gewöhnlichen Knoten, dieselbe Art, die dir `nodeNew` gibt, den du mit den Aufrufen, die du schon kennst, platzierst, drehst, skalierst und löschst. Instanziieren ist der billige Aufruf. Lade einen Baum, instanziiere ihn vierzigmal, und du hast einen Wald, mit einer Kopie der Formen auf der Grafikkarte und vierzig Stellen, an denen sie gezeichnet werden. Mit dem Modell kommt alles, was in der Datei war: die Formen, die Oberflächen und ihre Farben, die Anordnung der Teile, ein Skelett, falls es eins gibt, und Bewegungen, falls es welche gibt. Für ein geladenes Modell baust du keine Materialien; es hat seine eigenen mitgebracht. [#_scale_and_the_first_thing_that_goes_wrong] === Größe, und das Erste, was schiefgeht Der Drache ist ungefähr siebeneinhalb Einheiten hoch. Deine Kamera steht neun Einheiten entfernt. Darum füllt er das Fenster. [source,lua] ---- nodeSetScale(dragon, 0.5) ---- Füg das unter der Zeile mit `modelInstance` ein. Jetzt ist er keine vier Einheiten hoch, steht auf dem Boden und passt ins Bild. Das ist kein Detail; es ist das häufigste erste Problem mit einem Modell, und es hat zwei Gesichter. Ein Modell, das in Zentimetern gebaut wurde, kommt hundertmal zu groß an, und du siehst eine riesige Fläche, die den Bildschirm füllt, was meistens nach „meine Szene ist kaputt“ aussieht statt nach „ich stecke im Hals des Drachen“. Ein Modell, das in irgendeiner anderen Einheit gebaut wurde, kommt so klein an, dass es ein Punkt ist oder vor dem Hintergrund unsichtbar, und du nimmst an, dass die Datei nicht geladen wurde. Die Lösung ist immer dieselbe: eine Skalierung auf dem Knoten der Instanz. Nicht auf den Teilen, nicht im Modellierprogramm, wenn es sich vermeiden lässt, sondern ein `nodeSetScale` auf dem Knoten, den dir `modelInstance` gegeben hat, gewählt, indem du eine Zahl ausprobierst und hinsiehst. `0.01` und `100` sind beides völlig gewöhnliche Antworten. Wenn du nicht sagen kannst, wie groß etwas ist, verschaff dir ein Lineal. Stell eine Kiste von einer Einheit daneben: [source,lua] ---- local ruler = nodeNew() nodeSetMesh(ruler, meshBox(1, 1, 1), stone) nodeSetPosition(ruler, 3, 0.5, 0) ---- Ein Ding in Kistengröße neben dem Modell sagt dir auf einen Blick, ob das Modell ein Drache oder ein Berg ist. Nimm das Lineal wieder heraus, wenn du fertig bist. Wenn du ihn schon platzierst, dreh ihn so, dass er dich ansieht: [source,lua] ---- nodeSetRotation(dragon, 0, -60, 0) ---- Der Drache wurde mit Blick entlang des positiven X modelliert, also nach rechts, darum steht er am Anfang im Profil. Eine Drehung um Y bringt ihn herum zur Kamera. Die „Vorderseite“ eines Modells ist die Richtung, die die Person gewählt hat, die es gebaut hat, und um sie herauszufinden, musst du es drehen und hinsehen. [#_light] === Licht Starte, was du jetzt hast. Der Drache ist da, und er ist trostlos: eine flache, trübe Form ohne Glanzlichter und ohne Schatten. Das liegt daran, dass das einzige Licht in deiner Szene das Umgebungslicht ist, das schwache Leuchten von überall, das `sceneSetAmbient` einstellt. Das ist die allerhäufigste Enttäuschung in 3D, und sie passiert jedem: Das Modell wurde einwandfrei geladen, und die Szene ist schwarz oder fast schwarz, weil nichts darauf scheint. Bevor du die Datei verdächtigst, stell ein Licht hinein. Es gibt drei Arten, und jede ist ein Knoten, also platzierst du sie und richtest sie aus mit `nodeSetPosition` und `nodeLookAt`, wie alles andere. [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` ist eine Sonne. Sie wird behandelt, als wäre sie unendlich weit weg, also zählt nur die Richtung, in die sie scheint, und alles in der Szene bekommt gleich viel davon. Ihre Position ist dazu da, dass `nodeLookAt` sie damit ausrichten kann. Das ist das Licht, das einer Szene ihre Hauptform gibt, und die meisten Szenen wollen genau eins. `lightSetIntensity` ist die Helligkeit. Bei einem Richtungslicht macht `1` eine weiße Fläche, die ihm direkt zugewandt ist, ganz weiß, also ist `1.8` ein heller Tag. `lightSetColor` färbt es: ein leicht warmes Weiß für die Sonne, ein kaltes Blau für Mondlicht und der Abschnitt Licht in der Referenz für den Rest. `lightSetShadow(sun, true)` sorgt dafür, dass der Drache auf dem Boden sitzt, statt darüber zu schweben. Schatten sind standardmäßig aus, weil jedes Licht, das Schatten wirft, in jedem Frame einen weiteren Durchgang über die ganze Szene kostet. Schalte sie für das eine Licht ein, auf das es ankommt, und lass sie bei den anderen aus; ein kleiner Rechner wird es dir danken. [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` ist eine Glühbirne. Sie sitzt an einer Stelle, scheint in alle Richtungen und wird mit der Entfernung schwächer. Dieses Schwächerwerden ist der Grund, warum die Zahl `14` ist und nicht `1`: Die Intensität eines Punktlichts ist seine Helligkeit direkt am Licht, und wenn es ein paar Einheiten weit gekommen ist, ist nicht mehr viel übrig. Eine Glühbirne, die einen Raum beleuchtet, will meistens etwas im zweistelligen Bereich. Wenn ein Punktlicht scheinbar nichts tut, ist das fast immer der Grund. `lightSetRange` sagt, wie weit es reicht, bevor es ganz aufhört, und das verhindert, dass eine Lampe in einem Raum den nächsten schwach mitbeleuchtet, und kostet nichts. Die dritte Art ist `LIGHT_SPOT`, ein Kegel, der entlang des eigenen negativen Z des Knotens zeigt, mit `lightSetCone` für den Winkel der hellen Mitte und des weichen Rands: eine Taschenlampe, ein Scheinwerfer, ein Suchscheinwerfer. Bis zu acht Lichter leuchten gleichzeitig, und das ist mehr, als die meisten Szenen brauchen. Wenn du mehr willst, willst du eigentlich weniger. Starte es jetzt. Eine fast weiße Sonne von links, ein warmes orangefarbenes Leuchten von rechts, ein Schatten auf der Platte und ein Drache mit einer Vorder- und einer Rückseite. [#_materials_at_the_level_you_need_today] === Materialien, so weit du sie heute brauchst Der Drache hat seine eigenen Materialien mitgebracht, und es sind drei: ein dunkles Metall, ein helleres champagnerfarbenes Metall und ein blasses Glas, in das man ein Stückchen hineinsehen kann. Die meisten seiner Teile sind aus mehr als einem davon geschnitten. Du musst nichts davon anfassen, und meistens solltest du es auch nicht. Wenn du doch ändern willst, wie ein Teil eines Modells aussieht, machst du ein Material wie in Lektion zweiundzwanzig und legst es auf das Teil: [source,lua] ---- local red = materialNew() materialSetColor(red, 220, 50, 40) materialSetRoughness(red, 0.4) nodeSetMaterial(nodeFind("crest", dragon), red) ---- `nodeFind(name, root)` durchsucht eine Modellinstanz nach einem Teil mit dem Namen, den es in der Datei hatte, und gibt seinen Knoten zurück, oder `nil`, wenn es keinen solchen Namen gibt. So greifst du in ein Modell hinein: ein Geschützturm, ein Rad, eine Hand, an die man eine Fackel hängt, oder der Kamm des Drachen. Der Kamm ist ein Stück aus einem Material, also trägt sein Knoten die Form selbst, und die Zeile oben malt ihn neu an. Ein Teil, das aus mehreren Materialien gebaut ist, wird als Knoten mit einem Kind pro Stück gespeichert, und ein Material auf dem Elternknoten ändert nichts, was man sieht. Wenn ein `nodeSetMaterial` scheinbar nichts tut, ist das der Grund, und `nodeGetChildren` zeigt dir, was darunter liegt. Vier Zahlen beschreiben die meisten Oberflächen. Die Farbe, aus `materialSetColor`. `materialSetRoughness`, von `0` für einen Spiegel bis `1` für Kreide. `materialSetMetallic`, das für fast alles `0` ist -- Farbe, Holz, Plastik, Haut -- und `1` für echtes Metall, und die Werte dazwischen braucht man kaum. Und `materialSetEmissive`, das eine Oberfläche in ihrer eigenen Farbe leuchten lässt, für Bildschirme, Lampen und glühendes Eisen, ohne etwas drumherum zu beleuchten. Eins solltest du über Metall wissen: Eine Metalloberfläche zeigt dir ihre Spiegelungen statt einer eigenen Farbe, also sieht sie in einer Szene, in der es nichts zu spiegeln gibt, dunkel und stumpf aus. Darum braucht das dunkle Metall des Drachen die Sonne, um lebendig zu werden. Die richtige Lösung ist ein Himmel (`sceneSetSky`), der die Szene in ein Panorama hüllt, das hinter allem zu sehen ist und sie zugleich beleuchtet; „Das Aussehen des Frames“ in der Referenz behandelt das. [#_movement_the_model_brings_with_it] === Bewegung, die das Modell mitbringt Viele Modelle bringen Animationen mit: ein Gehen, ein Rennen, ein Herumstehen, eine Tür, die aufgeht. Sie sind in der Datei gespeichert, und eine abzuspielen ist ein einziger Aufruf auf dem Wurzelknoten der Instanz. [source,lua] ---- animationPlay(dragon, "Walk", true) ---- Das zweite Argument ist der Name des Clips, wie die Datei ihn speichert, oder seine Nummer. `true` heißt: endlos in einer Schleife. Zwei weitere, optionale Zahlen setzen die Geschwindigkeit und die Überblendung: `animationPlay(hero, "Run", true, 1.2, 0.4)` spielt das Rennen ein Fünftel schneller ab und blendet über vier Zehntelsekunden hinein, sodass aus einem Gehen ohne Ruck ein Rennen wird. `animationStop`, `animationPause` und `animationIsPlaying` tun, was ihre Namen sagen. Diese Zeile kannst du noch nicht ausführen, und hier ist der ehrliche Grund: Der Drache in deinem Ordner `Singe` hat keine Animationen. Er ist ein Modell, keine Vorstellung. Nach einem Clip zu fragen, der nicht da ist, hält das Spiel mit einem Fehler an, also nimm nie etwas an -- frag: [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` gibt eine Tabelle mit den Namen der Clips in der Datei zurück, der Reihe nach, also ist `clips[1]` der erste und `#clips` die Anzahl -- die Tabellen-Aufrufe aus Lektion sechs, auf einer Tabelle, die die Engine gefüllt hat. Starte es und beobachte die Konsole: Die Meldung erscheint, weil diese Anzahl null ist. Gewöhn dir diese Prüfung bei jedem Modell an, das du nicht selbst gemacht hast. Sie kostet drei Zeilen und macht aus „das Spiel ist beim Start gestorben“ einen Satz im Log. [#_moving_the_parts_yourself] === Die Teile selbst bewegen Ein Modell ohne Animationen ist keine Statue. Seine Teile sind Knoten, und Knoten drehen sich. [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 ---- Speichere. Die Flügel schlagen, der Drache dreht sich langsam auf der Stelle, und der Schatten auf dem Boden schlägt mit. `singeGetTicks` ist die Anzahl der Millisekunden, seit die Engine gestartet ist, also steigt sie für immer gleichmäßig an. `math.sin` nimmt diese steigende Zahl und gibt einen Wert zurück, der gleichmäßig von `-1` bis `1` und wieder zurück gleitet, ein voller Flügelschlag ungefähr alle anderthalb Sekunden bei diesem Teiler. Die Multiplikation mit `22` macht daraus einen Winkel von zweiundzwanzig Grad in jede Richtung. Teilst du durch eine größere Zahl, wird der Schlag langsamer; multiplizierst du mit einer größeren Zahl, wird er weiter. Diese eine Zeile ist die billigste Animation, die es gibt, und es lohnt sich, sie in der Tasche zu behalten. Die Flügel drehen sich um ihre eigene X-Achse, und zwar um die Schulter statt um die Mitte des Tiers, weil der, der die Datei gebaut hat, den Knoten jedes Teils dorthin gesetzt hat, wo das Gelenk hingehört. Ein Modell, das nicht so gebaut wurde, schwenkt seine Teile stattdessen um die Mitte des Modells, und die Lösung ist, den Knoten des Teils zu verschieben; der Abschnitt Knoten in der Referenz behandelt das. Achte darauf, dass das Drehen des ganzen Drachen dem Drehen der Flügel nicht in die Quere kommt. Die Teile hängen unter der Wurzel der Instanz, also nimmt eine Drehung der Wurzel alles mit, und die eigene Drehung jedes Flügels kommt obendrauf. Diese Anordnung -- ein Ding in einem Ding in einem Ding, und jedes bewegt sich in der Welt seines Elternknotens -- ist das, wofür ein Szenenbaum da ist. [#_your_own_models] === Deine eigenen Modelle Wenn du eigene machst, exportiere aus Blender -- oder Maya, 3ds Max oder irgendetwas anderem Modernen -- als *glTF 2.0 binary*, also als `.glb`-Datei. Das ist die Standardwahl von Blender und das einzige Modellformat, das Singe liest. Die eine Regel ist, dass die Datei in sich geschlossen sein muss: die Formen, die Bilder darauf und die Bewegungen, alles in der einen `.glb`. Eine Datei, die auf eine `.bin` oder eine `.png` daneben verweist, wird abgelehnt, mit einer Meldung, die nennt, was sie wollte. Der Exporter von Blender macht mit seinen Standardeinstellungen das Richtige. Nicht alles, was du bauen kannst, übersteht die Reise. Der Abschnitt „Modelle und Animation“ in der Referenz, im Kapitel 3D-Szenen, ist die Liste: was gelesen wird, was stillschweigend ignoriert wird, und die Grenzen (ein Skelett darf zum Beispiel bis zu hundertachtundzwanzig Gelenke haben). Lies ihn einmal, bevor du ein Wochenende in ein Modell steckst, nicht danach. [#_try_it_23] === Probier es aus . *Mach das Licht aus.* Kommentier den ganzen `sun`-Block aus, indem du `--` an den Anfang jeder Zeile setzt, und starte es. Dann mach dasselbe mit `sceneSetAmbient`. Merk dir, wie das aussieht, denn eines Tages passiert es, ohne dass du es wolltest. . *Beweg die Sonne.* Ändere `nodeLookAt(sun, 0, 1.5, 0)` in `nodeLookAt(sun, 0, 20, 0)`, sodass die Sonne über den Kopf des Drachen zielt. Sieh zu, wie der Schatten und die Glanzlichter verschwinden. Ein Licht auszurichten ist keine Verzierung. . *Heiz die Fackel an.* Erhöh die Intensität des Punktlichts von `14` auf `60` und senk seine Reichweite auf `4`. Dann setz es auf `(0, 6, 0)`, über den Drachen. Ein Punktlicht ist eine Glühbirne, und du entscheidest, wo die Glühbirne hängt. . *Instanziiere ihn zweimal.* Ruf `modelInstance(dragonModel)` ein zweites Mal auf, bewahr das Ergebnis in einer anderen Variablen auf, skalier es und setz es auf `(-4, 0, -3)`. Einmal geladen, zwei Drachen. Dann versuch stattdessen, das Modell zweimal zu laden, und versteh, warum du das nicht tun solltest. . *Finde ein anderes Teil.* Die Teile des Drachen heißen `head`, `jaw`, `neck`, `tail`, `body`, `crest`, `hornL`, `hornR`, `wingL`, `wingR`, `foreLegL`, `foreLegR`, `hindLegL` und `hindLegR`. Lass den Schwanz mit seinem eigenen `math.sin` in einer anderen Geschwindigkeit schwingen als die Flügel. Dann probier `nodeFind("wings", dragon)` und sieh, was ein falscher Name anrichtet, wenn du das `nil` direkt an `nodeSetRotation` weitergibst. [#_break_it_on_purpose_22] === Mach es absichtlich kaputt Ändere den Dateinamen in `Singe/Dragonmodel.glb` -- ein einziger Großbuchstabe Unterschied -- und starte es. Singe hält an, bevor das Spiel startet: ---- 13:modelLoad: Unable to read Singe/Dragonmodel.glb. ---- Die Zeile, der Aufruf und die Beschwerde, in dieser Reihenfolge. „Unable to read“ heißt, dass die Engine nachgesehen und nichts gefunden hat, und das ist fast immer eins von drei Dingen: Der Name ist falsch geschrieben, die Groß- und Kleinschreibung stimmt nicht, oder die Datei liegt nicht da, wo du gesagt hast. Unter Windows wäre die Groß- und Kleinschreibung verziehen worden, und dein Spiel würde dann bei allen unter Linux scheitern, und das ist ein schlimmerer Fehler als dieser. Tipp Namen genau. Dieselbe Meldung mit einem richtigen Dateinamen heißt, dass die Datei nicht dort liegt, wo die Engine nachgesehen hat. Der Ordner `Singe` wird in dem Ordner angelegt, *aus* dem du die Engine startest, also zeigt `Singe/DragonModel.glb` ebenfalls woandershin, wenn du Singe von woanders gestartet hast. [#_what_you_learned_23] === Was du gelernt hast * `modelLoad` liest eine `.glb` einmal; `modelInstance` setzt so oft, wie du willst, Kopien davon in die Szene. * Die Wurzel einer Instanz ist ein gewöhnlicher Knoten: Platzier, dreh, skalier und lösch ihn wie jeden anderen. * Ein Modell bringt seine eigenen Formen und Materialien mit. * Ein Modell kommt in der Größe an, die sein Autor benutzt hat, und darum gehört ein `nodeSetScale` auf der Instanz zum Platzieren dazu. * Eine Szene ohne Licht ist schwarz oder fast schwarz, und das ist das Erste, was du prüfst, wenn ein Modell falsch aussieht. * Ein Richtungslicht ist eine Sonne, ein Punktlicht ist eine Glühbirne, die mit der Entfernung schwächer wird und eine viel größere Intensität braucht, und ein Spotlicht ist ein Kegel. * Schatten sind aus, bis `lightSetShadow` sie einschaltet, und jeder kostet etwas. * `nodeFind` erreicht ein Teil eines Modells über seinen Namen aus der Datei. * `modelGetAnimations` sagt, was ein Modell abspielen kann, und nach einem Clip zu fragen, der nicht da ist, ist ein Fehler. * Ein Modell ohne Animationen lässt sich trotzdem Teil für Teil bewegen. [#_next_time_23] === Beim nächsten Mal Alles in dieser Szene bewegt sich, weil du es ihm gesagt hast, Frame für Frame. Die nächste Lektion gibt diese Arbeit an eine Physik-Engine ab: Kisten, die fallen, rollen, sich stapeln und sich gegenseitig umwerfen, ohne dass du eine Zeile Rechnerei schreibst -- und die schwierigere Frage, wann du sie das tun lassen solltest. Das ist Lektion vierundzwanzig.