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

422 lines
18 KiB
Text

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