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

447 lines
18 KiB
Text

[#_lesson_22_into_3d]
== Lección 22: Hacia el 3D
image::learn/22-3d.png[La lección terminada, 480]
Todo lo que has dibujado hasta ahora ha sido plano. Los sprites, el texto, las
cajas y las líneas viven en el overlay, que es una lámina de cristal en la parte
delantera de la ventana con las mismas coordenadas que tenía en la lección uno.
Esta lección pone un mundo detrás de ese cristal: un sitio con profundidad,
donde una cosa puede estar más cerca o más lejos, y donde una cámara decide qué
ves de él.
Al final tendrás una caja flotando en el espacio que puedes girar con las teclas
de flecha, y los números de sus ángulos impresos en la esquina con el mismo
`overlayPrint` que usaste el primer día. Esa última parte importa más de lo que
parece, y tiene su propia sección.
[#_a_scene_a_box_and_a_light]
=== Una escena, una caja y una luz
Crea una carpeta, pon dentro un archivo llamado `spin.singe` y escribe esto.
[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
----
Ejecútalo con `Singe -R spin`. Una caja naranja gira despacio sobre un fondo
azul oscuro, iluminada desde arriba y desde la derecha, con una cara brillante y
las demás perdiéndose en la sombra.
En ese script no hay ninguna cámara. Singe te ha puesto en `(0, 0, 5)` mirando
al centro del mundo, porque una escena sin cámara tiene que verse desde algún
sitio. Dentro de un momento lo sustituirás por tu propia cámara.
Si la ventana se queda negra, lee la consola. En una máquina cuyos gráficos no
pueden dibujar una escena 3D, `sceneEnable` detiene el juego con un mensaje que
lo dice, y no hay nada que hacer salvo seguir la lección en otro sitio. Los
juegos 2D siguen funcionando en una máquina así; el 3D no.
[#_what_just_happened_18]
=== Qué acaba de pasar
Las tres llamadas de arriba preparan el mundo en su conjunto.
[source,lua]
----
sceneEnable(true)
sceneSetBackground(18, 20, 34)
sceneSetAmbient(35, 35, 45)
----
`sceneEnable(true)` enciende la capa 3D. Es la única llamada que hacen todos los
juegos 3D, y hasta que la hagas no se dibuja nada de lo que construyas en la
escena.
`sceneSetBackground` es el color con el que se borra la escena en cada
fotograma, rojo, verde y azul de `0` a `255`, igual que han funcionado los
colores desde la lección nueve. Hay un cuarto número que puedes darle para la
transparencia, que es como un juego que reproduce vídeo pone un objeto 3D
delante de la película. Si no lo pones, el fondo es opaco.
`sceneSetAmbient` es un poco de luz que llega de todas las direcciones a la vez.
Su trabajo es evitar que los lados a los que no llega ninguna lámpara queden
completamente negros. Más adelante bájala a `0, 0, 0` y mira lo dura que se
vuelve la imagen.
[source,lua]
----
local paint = materialNew()
materialSetColor(paint, 215, 95, 60)
materialSetRoughness(paint, 0.6)
----
Un *material* es el aspecto de una superficie: su color, lo brillante que es, si
lleva una imagen encima. `materialNew` crea uno y te devuelve un número que lo
representa, el mismo tipo de handle que te dio `spriteLoad` en la lección nueve.
Guárdalo en una variable, porque todas las llamadas posteriores lo necesitan.
`materialSetRoughness` va de `0`, un espejo con un reflejo diminuto y duro, a
`1`, una superficie mate como de tiza sin ningún reflejo. `0.6` es pintura sobre
madera. Hay una docena más de llamadas `materialSet`, y la próxima lección usa
algunas; la sección Material de la referencia las enumera todas.
[source,lua]
----
local box = nodeNew()
nodeSetMesh(box, meshBox(2, 2, 2), paint)
----
Esta es la importante. Un *nodo* es un sitio en el mundo. Tiene una posición,
una rotación y una escala, y por sí solo no dibuja absolutamente nada: es un
punto al que se le pueden enganchar cosas. Todo en una escena 3D es un nodo,
incluidas las luces y la cámara.
`nodeNew` crea uno vacío en el centro del mundo. `nodeSetMesh` le cuelga dos
cosas: una *malla*, que es la forma, y el material, que es el aspecto de esa
forma. `meshBox(2, 2, 2)` construye una caja de dos unidades de ancho, dos de
alto y dos de fondo, y te devuelve un handle a ella igual que hizo
`materialNew`.
Vale la pena compartir las mallas y los materiales. Un mismo handle de `meshBox`
lo pueden usar cien nodos, y así es como se dibujan cien cajas sin que cueste
caro. Hacer cien cajas separadas que resultan tener el mismo tamaño es la forma
de principiante de volver lenta una escena.
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 4, 6, 5)
nodeLookAt(sun, 0, 0, 0)
----
`lightNew` también crea un nodo -- uno que lleva una luz -- y te devuelve su
handle de nodo, así que lo colocas y lo apuntas con las mismas llamadas que usas
para todo lo demás. `LIGHT_DIRECTIONAL` es un sol: está tan lejos que solo
cuenta la dirección en la que brilla, y por eso aquí la posición solo importa
porque `nodeLookAt` la usa para calcular la dirección.
Quita esas tres líneas y vuelve a ejecutarlo. La caja se queda casi negra,
porque la única luz que queda es la tenue luz ambiente. La lección veintitrés
trata entera de la luz, y este es tu primer contacto con la decepción más común
del 3D: el modelo se ha cargado bien, y la escena está negra porque no le da
ninguna luz.
[source,lua]
----
nodeRotate(box, 0, 0.5, 0)
----
`nodeRotate` gira un nodo tantos grados alrededor de cada uno de sus propios
tres ejes, además de la rotación que ya tuviera. Medio grado por fotograma, a
unos sesenta fotogramas por segundo, es una vuelta cada doce segundos. Es el
mismo truco de la lección dos: un cambio pequeño, aplicado en cada fotograma, se
ve como movimiento.
[#_which_way_is_which]
=== Hacia dónde va cada eje
Tres números describen un sitio en la escena, y tienes que saber qué significa
cada uno antes de poder poner nada en ningún sitio.
* *X* va hacia la derecha. Una X mayor está más a la derecha.
* *Y* va hacia arriba. Una Y mayor está más alta. El suelo de una escena suele
ser `y = 0`.
* *Z* va hacia ti, saliendo de la pantalla. Una Z mayor está más cerca de la
cámara por defecto, y *la Z negativa es la dirección hacia la que mira la
cámara*.
Esa última pilla a todo el mundo. Las cosas que quieres delante de la cámara van
en Z negativa. No es una manía de Singe: glTF y Blender usan la misma
disposición, así que un modelo exportado desde Blender llega mirando hacia donde
Singe espera.
No te fíes de mi palabra en nada de esto. Añade esta línea después de la línea
`nodeSetMesh`, guarda y mira:
[source,lua]
----
nodeSetPosition(box, 3, 1, 0)
----
La caja salta hacia la derecha y hacia arriba. `nodeSetPosition` pone un nodo en
un sitio exacto, y sustituye la posición que tuviera el nodo. Hay un
`nodeMove(node, dx, dy, dz)` a juego que, en cambio, desplaza un nodo una
cantidad, a lo largo de sus propios ejes, así que `nodeMove(ship, 0, 0, -0.1)`
lleva una nave hacia delante apunte hacia donde apunte.
Prueba después `nodeSetPosition(box, 0, 0, -20)`. La caja se encoge en la
distancia, que es la Z negativa haciendo su trabajo. Luego prueba `(0, 0, 20)`:
la caja desaparece, porque la has puesto detrás de la cámara.
Dos llamadas más completan el conjunto.
[source,lua]
----
nodeSetRotation(box, 0, 45, 0)
nodeSetScale(box, 0.5)
----
`nodeSetRotation` fija la rotación directamente en grados, mientras que
`nodeRotate` le suma. Los tres números son alrededor de X, alrededor de Y y
alrededor de Z: girar alrededor de Y es lo que hace una persona de pie en el
suelo cuando se gira para mirar hacia otro lado, y es el que más vas a usar.
`nodeSetScale` hace el nodo más grande o más pequeño, con un número para los
tres ejes o con tres, uno para cada uno.
Quita esos dos experimentos antes de seguir. El resto de la lección parte del
script tal como lo escribiste al principio.
[#_your_own_camera]
=== Tu propia cámara
La vista por defecto es una cortesía, no una característica. Pon una cámara.
[source,lua]
----
local camera = nodeNew()
nodeSetPosition(camera, 0, 2, 7)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
cameraSetPerspective(60, 0.1, 100)
----
Ponlo debajo de la luz, antes de tu `onOverlayUpdate`, y ejecútalo. Ahora miras
la caja un poco desde arriba, desde dos unidades más arriba y siete más atrás.
Una cámara es un nodo como cualquier otro. `cameraSet` le dice al motor desde
qué nodo mirar, y la escena se dibuja desde la posición de ese nodo mirando a lo
largo de la Z negativa del propio nodo. Apuntar una cámara a mano con
`nodeSetRotation` sería un suplicio, así que `nodeLookAt` lo hace por ti: dale
un punto del mundo y gira el nodo para que lo mire.
`cameraSetPerspective` es el objetivo. El primer número es el campo de visión en
grados, de arriba abajo: cuánto mundo se mete en la altura de la ventana. Un
número pequeño como `35` es un teleobjetivo que lo aplana todo; uno grande como
`90` es un gran angular que hace que la habitación parezca enorme y que las
esquinas se abomben. `60` es un valor por defecto cómodo. Los otros dos números
son las distancias más cercana y más lejana que se dibujan, y lo que queda fuera
de ese rango no se dibuja en absoluto.
Como la cámara es un nodo normal, todo lo que has aprendido sobre los nodos
funciona con ella. Más adelante colgarás una cámara debajo de un coche en
movimiento y tendrás una cámara de persecución gratis.
[#_turning_it_with_the_arrow_keys]
=== Girarla con las teclas de flecha
Leer el sistema de coordenadas en una página es una mala forma de aprenderlo.
Mover algo dentro de él es una buena. Sustituye tu `onOverlayUpdate` por esto, y
añade encima los dos callbacks nuevos y las cuatro variables.
[source,lua]
----
local turnLeft = false
local turnRight = false
local tiltUp = false
local tiltDown = false
----
Esas van al principio del todo del archivo, encima de todo lo demás.
[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` y `onInputReleased` son callbacks, como `onOverlayUpdate`: los
escribes tú, y Singe los llama cuando un control se pulsa o se vuelve a soltar.
Una pulsación llega una sola vez, no una y otra vez mientras se mantiene la
tecla, y por eso cada uno pone una variable y el callback de dibujo hace el
trabajo. Ese patrón -- la pulsación pone un indicador, el fotograma lee el
indicador -- es como sacas «mientras se mantiene la tecla» de un motor que solo
te cuenta los cambios.
Los nombres `SWITCH_` vienen del propio motor, así que no hay que cargar nada
para usarlos. Tampoco son teclas. Son lo que *significa* un control, y
`controls.cfg` decide qué tecla, botón o dirección del stick produce cada uno.
Hoy tus teclas de flecha envían `SWITCH_LEFT` y compañía; el stick de un mando
envía lo mismo, y tu script nunca nota la diferencia.
Los signos están elegidos para que cada flecha mueva la cara que estás mirando
en la dirección que has pulsado. Girar alrededor de Y un ángulo positivo lleva
esa cara cercana hacia la derecha, así que la flecha izquierda pide uno
negativo. Si te parece al revés, cambia los signos; son tus controles, no los
del motor.
[#_the_overlay_is_still_on_top]
=== El overlay sigue encima
Añade estas líneas a `onOverlayUpdate`, encima del `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))
----
Ahí no hay nada nuevo salvo aquello sobre lo que se imprime. `overlayPrint` es
la llamada de la lección uno, que cuenta en celdas de carácter y no necesita
ninguna fuente cargada, y está dibujando encima de una escena 3D sin que nadie
le haya dicho nada.
Así es como está montado, y vale la pena decirlo claramente: Singe dibuja
primero el vídeo, luego la escena 3D encima, y luego el overlay encima de eso.
Tu puntuación, tus vidas, tu cronómetro, tu punto de mira y tus subtítulos son
todo trabajo del overlay, exactamente igual que en la segunda parte. Un juego 3D
pone su puntuación en la esquina con las mismas cinco líneas que usa un juego
2D.
`nodeGetRotation` devuelve tres números, los ángulos alrededor de X, de Y y de
Z, y esta línea guarda los dos primeros en `pitch` y `yaw`. Son sus nombres
habituales (en español, cabeceo y guiñada): pitch es el morro que sube y baja, y
yaw es girar a izquierda y derecha. `math.floor` tira los decimales para que los
números dejen de parpadear.
Mira el yaw mientras mantienes pulsada la flecha izquierda. Baja más allá de
`-180` y vuelve a aparecer por `180`, porque los ángulos se dan como el giro más
corto que te lleva ahí y no como un total acumulado. Si tu juego necesita saber
cuántas vueltas ha dado el jugador, cuéntalas tú.
[#_try_it_22]
=== Pruébalo
. *Mueve la cámara.* Cambia `nodeSetPosition(camera, 0, 2, 7)` por
`(0, 8, 7)`, luego por `(7, 2, 7)` y luego por `(0, 0.2, 7)`. Cada vez, el
`nodeLookAt` mantiene la caja centrada, así que consigues otro ángulo de la
misma escena cambiando un número.
. *Cambia el objetivo.* Prueba `cameraSetPerspective(30, 0.1, 100)`, luego
`(90, 0.1, 100)`, sin mover la cámara. Un juego parece rápido con un campo de
visión amplio y majestuoso con uno estrecho, y este es el regulador de
ambiente más barato que existe.
. *Pon una segunda caja.* Copia las dos líneas que crearon la caja, cambia el
nombre de la variable a `mark` y dale `nodeSetPosition(mark, 3, 0, 0)` y
`nodeSetScale(mark, 0.3)`. Ahora tienes una referencia con la que juzgar la
primera caja. Muévela a `(0, 3, 0)` y luego a `(0, 0, -3)` y di en voz alta
qué eje es cuál.
. *Comparte la malla.* En lugar de llamar dos veces a `meshBox`, guarda la
primera en una variable y dale el mismo handle a los dos nodos. No se ve nada
distinto. De eso se trata: una forma, dos sitios.
. *Cuelga la cámara de la caja.* Conserva la segunda caja del paso anterior, o
no quedará nada con lo que comparar el movimiento. Luego busca
`nodeSetParent` en la referencia y cuelga la cámara debajo de la primera caja.
Mantén pulsada una tecla de flecha: la cámara acompaña la rotación de la caja,
así que la caja se queda quieta y el resto de la escena pasa girando. Eso está
a una línea de una vista en primera persona, y también es como descubres que
una cámara de persecución quiere un padre y no aritmética.
[#_break_it_on_purpose_21]
=== Rómpelo a propósito
Escribe mal el nombre del material. Cambia la línea `nodeSetMesh` para que use
`pain` en lugar de `paint`:
[source,lua]
----
nodeSetMesh(box, meshBox(2, 2, 2), pain)
----
Singe se detiene antes de que arranque el juego e imprime algo así:
----
15:nodeSetMesh: Argument 3 must be a number.
----
Este error tiene una forma distinta del de la lección uno, y viene del motor y
no de Lua. El número del principio es la línea. El nombre que va detrás es la
llamada que se ha quejado. La queja en sí es que la tercera cosa que le has
dado no era un número.
Vale la pena seguir el motivo, porque explica toda una familia de errores.
`pain` es una variable a la que nunca se le ha dado un valor, y en Lua una
variable vacía es `nil` -- el valor que significa «aquí no hay nada». Pasarle
`nil` a una llamada que quiere un handle de material da exactamente esto. Cuando
veas «must be a number» en una llamada del motor, busca en esa línea un nombre
de variable mal escrito, o un handle que te olvidaste de guardar.
[#_what_you_learned_22]
=== Lo que has aprendido
* `sceneEnable(true)` enciende la capa 3D, y no se dibuja nada en 3D hasta que
la llamas.
* Un nodo es un sitio en el mundo con una posición, una rotación y una escala.
Las mallas, las luces y las cámaras cuelgan todas de nodos.
* Una malla es la forma y un material es el aspecto, y cualquier número de nodos
puede compartir una de cada.
* X es la derecha, Y es arriba, y la Z negativa es hacia donde mira la cámara.
* `nodeSetPosition` y `nodeSetRotation` fijan un valor directamente; `nodeMove`
y `nodeRotate` lo cambian en una cantidad.
* Cualquier nodo puede ser la cámara. `nodeLookAt` la apunta y `cameraSet` la
elige.
* El campo de visión de `cameraSetPerspective` cambia cómo se siente la escena
sin mover nada.
* Una escena sin luz está negra, y es lo primero que hay que comprobar cuando no
aparece nada.
* El overlay sigue ahí, sigue encima y sigue siendo donde va tu puntuación.
[#_next_time_22]
=== La próxima vez
Como dragón, una caja deja bastante que desear. La próxima lección carga un
modelo de verdad -- uno que el motor ya desempaquetó en tu carpeta de trabajo,
así que no hay nada que descargar ni nada que dibujar -- lo coloca en el suelo y
lo ilumina como es debido. Esa es la lección veintitrés.