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