[#_lesson_22_into_3d] == Lektion 22: Hinein in 3D image::learn/22-3d.png[Die fertige Lektion, 480] Alles, was du bisher gezeichnet hast, war flach. Sprites, Text, Kästen und Linien leben alle auf dem Overlay, einer Glasscheibe vorn im Fenster mit denselben Koordinaten wie in Lektion eins. Diese Lektion stellt eine Welt hinter diese Scheibe: einen Ort mit Tiefe, an dem ein Ding näher oder weiter weg sein kann und an dem eine Kamera entscheidet, was du davon siehst. Am Ende hast du eine Kiste, die im Raum schwebt und die du mit den Pfeiltasten drehen kannst, und die Zahlen für ihre Winkel stehen in der Ecke, ausgegeben mit demselben `overlayPrint`, das du an deinem allerersten Tag benutzt hast. Dieser letzte Teil ist wichtiger, als er klingt, und es gibt einen Abschnitt dazu. [#_a_scene_a_box_and_a_light] === Eine Szene, eine Kiste und ein Licht Leg einen Ordner an, leg eine Datei namens `spin.singe` hinein und tipp das ein. [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 ---- Starte es mit `Singe -R spin`. Eine orangefarbene Kiste dreht sich langsam vor einem dunkelblauen Hintergrund, beleuchtet von oben rechts, mit einer hellen Seite, während die anderen in den Schatten abfallen. In diesem Skript gibt es keine Kamera. Singe hat dich auf `(0, 0, 5)` gesetzt, mit Blick auf die Mitte der Welt, weil eine Szene ohne Kamera ja von irgendwoher gesehen werden muss. Gleich ersetzt du das durch deine eigene Kamera. Wenn das Fenster schwarz bleibt, lies die Konsole. Auf einem Rechner, dessen Grafik keine 3D-Szene zeichnen kann, hält `sceneEnable` das Spiel mit einer Meldung an, die das sagt, und dagegen kannst du nichts tun, außer die Lektion woanders zu starten. 2D-Spiele funktionieren auf so einem Rechner weiter; 3D nicht. [#_what_just_happened_18] === Was gerade passiert ist Die drei Aufrufe ganz oben richten die Welt als Ganzes ein. [source,lua] ---- sceneEnable(true) sceneSetBackground(18, 20, 34) sceneSetAmbient(35, 35, 45) ---- `sceneEnable(true)` schaltet die 3D-Ebene ein. Das ist der eine Aufruf, den jedes 3D-Spiel macht, und bis du ihn machst, wird nichts gezeichnet, was du in der Szene baust. `sceneSetBackground` ist die Farbe, mit der die Szene in jedem Frame gelöscht wird, Rot, Grün und Blau von `0` bis `255`, so wie Farben seit Lektion neun funktionieren. Du kannst ihm eine vierte Zahl für die Transparenz geben, und so stellt ein Spiel, das Video abspielt, ein 3D-Objekt vor den Film. Lässt du sie weg, ist der Hintergrund undurchsichtig. `sceneSetAmbient` ist ein bisschen Licht, das aus allen Richtungen gleichzeitig kommt. Seine Aufgabe ist, die Seiten, die keine Lampe erreicht, davor zu bewahren, rein schwarz zu sein. Dreh es später auf `0, 0, 0` herunter und sieh, wie hart das Bild wird. [source,lua] ---- local paint = materialNew() materialSetColor(paint, 215, 95, 60) materialSetRoughness(paint, 0.6) ---- Ein *Material* ist, wie eine Oberfläche aussieht: ihre Farbe, wie glänzend sie ist, ob ein Bild darauf ist. `materialNew` erzeugt eins und gibt eine Zahl zurück, die dafür steht, dieselbe Art von Handle, die dir `spriteLoad` in Lektion neun gegeben hat. Bewahr es in einer Variablen auf, denn jeder spätere Aufruf braucht es. `materialSetRoughness` reicht von `0`, einem Spiegel mit einem winzigen, harten Glanzpunkt, bis `1`, einer kreidigen, matten Oberfläche ganz ohne Glanzpunkt. `0.6` ist Farbe auf Holz. Es gibt noch ein Dutzend weitere `materialSet`-Aufrufe, und die nächste Lektion benutzt ein paar davon; der Abschnitt Material in der Referenz listet sie alle auf. [source,lua] ---- local box = nodeNew() nodeSetMesh(box, meshBox(2, 2, 2), paint) ---- Das ist der wichtige Teil. Ein *Knoten* ist ein Ort in der Welt. Er hat eine Position, eine Drehung und eine Skalierung, und für sich allein zeichnet er überhaupt nichts: Er ist eine Stelle, an die man Dinge hängen kann. Alles in einer 3D-Szene ist ein Knoten, auch die Lichter und die Kamera. `nodeNew` erzeugt einen leeren Knoten in der Mitte der Welt. `nodeSetMesh` hängt zwei Dinge daran: ein *Mesh*, das ist die Form, und das Material, das ist, wie diese Form aussieht. `meshBox(2, 2, 2)` baut eine Kiste, zwei Einheiten breit, zwei hoch und zwei tief, und gibt ein Handle darauf zurück, so wie `materialNew` es getan hat. Meshes und Materialien lohnt es sich zu teilen. Ein `meshBox`-Handle kann von hundert Knoten benutzt werden, und so zeichnest du hundert Kisten billig. Hundert einzelne Kisten zu bauen, die zufällig gleich groß sind, ist der Anfängerweg, eine Szene langsam zu machen. [source,lua] ---- local sun = lightNew(LIGHT_DIRECTIONAL) nodeSetPosition(sun, 4, 6, 5) nodeLookAt(sun, 0, 0, 0) ---- `lightNew` erzeugt ebenfalls einen Knoten -- einen, der ein Licht trägt -- und gibt sein Knoten-Handle zurück, also stellst du es mit denselben Aufrufen auf und richtest es aus, die du für alles andere benutzt. `LIGHT_DIRECTIONAL` ist eine Sonne: Sie ist so weit weg, dass nur die Richtung zählt, in die sie scheint, und darum ist die Position hier nur insofern wichtig, als `nodeLookAt` aus ihr die Richtung ausrechnet. Nimm diese drei Zeilen heraus und starte es noch einmal. Die Kiste wird fast schwarz, weil nur noch das schwache Umgebungslicht übrig ist. Um Licht geht es in der ganzen Lektion dreiundzwanzig, und das hier ist ein erster Vorgeschmack auf die häufigste Enttäuschung in 3D: Das Modell wurde einwandfrei geladen, und die Szene ist schwarz, weil nichts darauf scheint. [source,lua] ---- nodeRotate(box, 0, 0.5, 0) ---- `nodeRotate` dreht einen Knoten um so und so viele Grad um jede seiner eigenen drei Achsen, zusätzlich zu der Drehung, die er schon hatte. Ein halbes Grad pro Frame, bei etwa sechzig Frames pro Sekunde, ist eine Umdrehung alle zwölf Sekunden. Das ist derselbe Trick wie in Lektion zwei: Eine kleine Änderung, in jedem Frame angewandt, wirkt wie Bewegung. [#_which_way_is_which] === Welche Richtung ist welche Drei Zahlen beschreiben einen Ort in der Szene, und du musst wissen, was jede davon bedeutet, bevor du irgendetwas irgendwohin stellen kannst. * *X* geht nach rechts. Größeres X ist weiter rechts. * *Y* geht nach oben. Größeres Y ist höher. Der Boden einer Szene ist meistens `y = 0`. * *Z* geht auf dich zu, aus dem Bildschirm heraus. Größeres Z ist näher an der Standardkamera, und *negatives Z ist die Richtung, in die die Kamera schaut*. Das Letzte erwischt jeden. Dinge, die vor der Kamera sein sollen, kommen auf negatives Z. Das ist keine Marotte von Singe: glTF und Blender benutzen dieselbe Anordnung, also kommt ein Modell, das aus Blender exportiert wurde, so ausgerichtet an, wie Singe es erwartet. Glaub mir nichts davon einfach so. Füg diese Zeile nach der Zeile mit `nodeSetMesh` ein, speichere und sieh hin: [source,lua] ---- nodeSetPosition(box, 3, 1, 0) ---- Die Kiste springt nach rechts und nach oben. `nodeSetPosition` setzt einen Knoten an eine genaue Stelle und ersetzt die Position, die er vorher hatte. Passend dazu gibt es `nodeMove(node, dx, dy, dz)`, das einen Knoten stattdessen um einen Betrag verschiebt, entlang seiner eigenen Achsen, also fährt `nodeMove(ship, 0, 0, -0.1)` ein Schiff vorwärts, egal in welche Richtung es gerade zeigt. Probier als Nächstes `nodeSetPosition(box, 0, 0, -20)`. Die Kiste schrumpft in die Ferne, und das ist negatives Z, das seine Arbeit macht. Dann probier `(0, 0, 20)`: Die Kiste verschwindet, weil du sie hinter die Kamera gestellt hast. Zwei weitere Aufrufe machen den Satz komplett. [source,lua] ---- nodeSetRotation(box, 0, 45, 0) nodeSetScale(box, 0.5) ---- `nodeSetRotation` setzt die Drehung direkt in Grad, während `nodeRotate` etwas dazuzählt. Die drei Zahlen sind um X, um Y und um Z: Eine Drehung um Y macht ein Mensch, der auf dem Boden steht und sich in eine andere Richtung dreht, und die wirst du am häufigsten benutzen. `nodeSetScale` macht den Knoten größer oder kleiner, entweder mit einer Zahl für alle drei Achsen oder mit dreien, einer für jede. Nimm beide Experimente wieder heraus, bevor du weitermachst. Der Rest der Lektion geht von dem Skript aus, wie du es zuerst getippt hast. [#_your_own_camera] === Deine eigene Kamera Die Standardansicht ist eine Höflichkeit, keine Funktion. Stell eine Kamera hinein. [source,lua] ---- local camera = nodeNew() nodeSetPosition(camera, 0, 2, 7) nodeLookAt(camera, 0, 0, 0) cameraSet(camera) cameraSetPerspective(60, 0.1, 100) ---- Setz das unter das Licht, vor dein `onOverlayUpdate`, und starte es. Jetzt schaust du leicht von oben auf die Kiste, aus zwei Einheiten Höhe und sieben Einheiten Abstand. Eine Kamera ist ein Knoten wie jeder andere. `cameraSet` sagt der Engine, aus welchem Knoten sie herausschauen soll, und die Szene wird von der Position dieses Knotens aus gezeichnet, mit Blick entlang seines eigenen negativen Z. Eine Kamera von Hand mit `nodeSetRotation` auszurichten wäre eine Qual, also erledigt `nodeLookAt` das für dich: Gib ihm einen Punkt in der Welt, und es dreht den Knoten so, dass er dorthin schaut. `cameraSetPerspective` ist das Objektiv. Die erste Zahl ist das Sichtfeld in Grad, von oben nach unten: wie viel von der Welt in die Höhe des Fensters gequetscht wird. Eine kleine Zahl wie `35` ist ein Teleobjektiv, das alles flach drückt; eine große wie `90` ist ein Weitwinkel, der den Raum riesig aussehen lässt und die Ecken ausbeult. `60` ist ein angenehmer Standardwert. Die anderen beiden Zahlen sind die kleinste und die größte Entfernung, die gezeichnet wird, und Dinge außerhalb dieses Bereichs werden überhaupt nicht gezeichnet. Weil die Kamera ein gewöhnlicher Knoten ist, funktioniert alles, was du über Knoten gelernt hast, auch bei ihr. Später hängst du eine Kamera unter ein fahrendes Auto und bekommst eine Verfolgerkamera geschenkt. [#_turning_it_with_the_arrow_keys] === Mit den Pfeiltasten drehen Das Koordinatensystem von einer Buchseite abzulesen ist ein schlechter Weg, es zu lernen. Etwas darin herumzubewegen ist ein guter. Ersetz dein `onOverlayUpdate` hiermit und füg die beiden neuen Callbacks und die vier Variablen darüber ein. [source,lua] ---- local turnLeft = false local turnRight = false local tiltUp = false local tiltDown = false ---- Die kommen ganz oben in die Datei, über alles andere. [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` und `onInputReleased` sind Callbacks wie `onOverlayUpdate`: Du schreibst sie, und Singe ruft sie auf, wenn ein Steuerelement gedrückt oder wieder losgelassen wird. Ein Druck kommt einmal an, nicht immer wieder, solange die Taste gehalten wird, und darum setzt jeder eine Variable, und der Zeichen-Callback erledigt die Arbeit. Dieses Muster -- der Druck setzt ein Flag, der Frame liest das Flag -- ist der Weg, aus einer Engine, die dir nur Änderungen meldet, ein „solange die Taste gehalten wird“ herauszuholen. Die Namen mit `SWITCH_` kommen von der Engine selbst, also muss nichts geladen werden, um sie zu benutzen. Sie sind auch keine Tasten. Sie sind das, was ein Steuerelement *bedeutet*, und `controls.cfg` entscheidet, welche Taste, welcher Knopf oder welche Stick-Richtung jeden davon erzeugt. Deine Pfeiltasten schicken heute `SWITCH_LEFT` und seine Freunde; der Stick eines Gamepads schickt dasselbe, und dein Skript merkt den Unterschied nie. Die Vorzeichen sind so gewählt, dass jeder Pfeil die Seite, auf die du schaust, in die Richtung bewegt, die du gedrückt hast. Eine Drehung um Y um einen positiven Winkel schwenkt diese vordere Seite nach rechts, also verlangt der linke Pfeil einen negativen. Wenn dir das verkehrt herum vorkommt, tausch die Vorzeichen; es ist deine Steuerung, nicht die der Engine. [#_the_overlay_is_still_on_top] === Das Overlay liegt immer noch oben Füg diese Zeilen in `onOverlayUpdate` über dem `return` ein. [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)) ---- Neu ist daran nichts außer dem, worüber es ausgegeben wird. `overlayPrint` ist der Aufruf aus Lektion eins, der in Zeichenzellen zählt und keine geladene Schriftart braucht, und er zeichnet über eine 3D-Szene, ohne dass man ihm irgendetwas davon gesagt hat. So ist die Anordnung, und es lohnt sich, es klar auszusprechen: Singe zeichnet zuerst das Video, dann die 3D-Szene darüber, dann das Overlay darüber. Deine Punktzahl, deine Leben, dein Timer, dein Fadenkreuz und deine Untertitel sind alles Arbeit fürs Overlay, genau wie in Teil zwei. Ein 3D-Spiel zeigt seine Punktzahl in der Ecke mit denselben fünf Zeilen, die ein 2D-Spiel benutzt. `nodeGetRotation` gibt drei Zahlen zurück, die Winkel um X, Y und Z, und diese Zeile behält die ersten beiden in `pitch` und `yaw`. Das sind die üblichen Namen dafür: Pitch ist die Nase, die hoch- und runtergeht, Yaw ist das Drehen nach links und rechts. `math.floor` wirft die Nachkommastellen weg, damit die Zahlen aufhören zu flackern. Beobachte den Yaw, während du den linken Pfeil gedrückt hältst. Er zählt über `-180` hinaus herunter und kommt von `180` wieder herum, weil die Winkel als die kleinste Drehung gemeldet werden, die dorthin führt, und nicht als laufende Summe. Wenn dein Spiel wissen muss, wie oft sich der Spieler gedreht hat, zähl das selbst mit. [#_try_it_22] === Probier es aus . *Beweg die Kamera.* Ändere `nodeSetPosition(camera, 0, 2, 7)` in `(0, 8, 7)`, dann in `(7, 2, 7)`, dann in `(0, 0.2, 7)`. Jedes Mal hält `nodeLookAt` die Kiste in der Mitte, also bekommst du für eine einzige Zahl einen anderen Blick auf dieselbe Szene. . *Wechsle das Objektiv.* Probier `cameraSetPerspective(30, 0.1, 100)`, dann `(90, 0.1, 100)`, ohne die Kamera zu bewegen. Ein Spiel fühlt sich mit einem weiten Sichtfeld schnell an und mit einem engen gemessen, und das ist der billigste Stimmungsregler, den es gibt. . *Stell eine zweite Kiste hinein.* Kopier die zwei Zeilen, die die Kiste gemacht haben, benenn die Variable in `mark` um und gib ihr `nodeSetPosition(mark, 3, 0, 0)` und `nodeSetScale(mark, 0.3)`. Jetzt hast du einen Orientierungspunkt, an dem du die erste Kiste messen kannst. Stell sie nacheinander auf `(0, 3, 0)` und `(0, 0, -3)` und sag laut, welche Achse welche ist. . *Teil das Mesh.* Statt `meshBox` zweimal aufzurufen, bewahr das erste in einer Variablen auf und gib beiden Knoten dasselbe Handle. Nichts sieht anders aus. Genau darum geht es: eine Form, zwei Orte. . *Häng die Kamera an die Kiste.* Behalte die zweite Kiste aus dem letzten Schritt, sonst bleibt nichts übrig, woran man die Bewegung sehen kann. Dann schlag `nodeSetParent` in der Referenz nach und häng die Kamera unter die erste Kiste. Halte eine Pfeiltaste gedrückt: Die Kamera fährt die Drehung der Kiste mit, also steht die Kiste still, und der Rest der Szene schwenkt vorbei. Das ist eine Zeile von einer Ego-Perspektive entfernt, und so findest du auch heraus, dass eine Verfolgerkamera einen Elternknoten will statt Rechnerei. [#_break_it_on_purpose_21] === Mach es absichtlich kaputt Vertipp dich beim Namen des Materials. Ändere die Zeile mit `nodeSetMesh` so, dass sie `pain` statt `paint` benutzt: [source,lua] ---- nodeSetMesh(box, meshBox(2, 2, 2), pain) ---- Singe hält an, bevor das Spiel startet, und gibt etwas aus wie: ---- 15:nodeSetMesh: Argument 3 must be a number. ---- Das ist eine andere Form von Fehler als die in Lektion eins, und er kommt von der Engine statt von Lua. Die Zahl vorn ist die Zeile. Der Name danach ist der Aufruf, der sich beschwert hat. Die Beschwerde selbst ist, dass das dritte Ding, das du ihm gegeben hast, keine Zahl war. Es lohnt sich, dem Grund nachzugehen, denn er erklärt eine ganze Familie von Fehlern. `pain` ist eine Variable, die nie einen Wert bekommen hat, und in Lua ist eine leere Variable `nil` -- der Wert, der „hier ist nichts“ bedeutet. Wenn du `nil` an einen Aufruf übergibst, der ein Material-Handle will, bekommst du genau das. Wenn du von einem Engine-Aufruf „must be a number“ siehst, such in dieser Zeile nach einem falsch geschriebenen Variablennamen oder nach einem Handle, das du vergessen hast aufzubewahren. [#_what_you_learned_22] === Was du gelernt hast * `sceneEnable(true)` schaltet die 3D-Ebene ein, und in 3D wird nichts gezeichnet, bis du es aufrufst. * Ein Knoten ist ein Ort in der Welt mit einer Position, einer Drehung und einer Skalierung. Meshes, Lichter und Kameras hängen alle an Knoten. * Ein Mesh ist die Form und ein Material das Aussehen, und jedes davon kann von beliebig vielen Knoten geteilt werden. * X ist rechts, Y ist oben, und negatives Z ist die Richtung, in die die Kamera schaut. * `nodeSetPosition` und `nodeSetRotation` setzen einen Wert direkt; `nodeMove` und `nodeRotate` ändern ihn um einen Betrag. * Jeder Knoten kann die Kamera sein. `nodeLookAt` richtet sie aus, und `cameraSet` wählt sie. * Das Sichtfeld in `cameraSetPerspective` ändert, wie sich die Szene anfühlt, ohne etwas zu bewegen. * Eine Szene ohne Licht ist schwarz, und das ist das Erste, was du prüfst, wenn nichts erscheint. * Das Overlay ist immer noch da, immer noch oben und immer noch der Ort für deine Punktzahl. [#_next_time_22] === Beim nächsten Mal Eine Kiste ist ein armseliger Drache. Die nächste Lektion lädt ein echtes Modell -- eines, das die Engine schon in deinen Arbeitsordner entpackt hat, also gibt es nichts herunterzuladen und nichts zu zeichnen -- stellt es auf den Boden und beleuchtet es richtig. Das ist Lektion dreiundzwanzig.