singe/docs/es/lessons/23-models.adoc

452 lines
19 KiB
Text

[#_lesson_23_models_and_light]
== Lección 23: Modelos y luz
image::learn/23-models.png[La lección terminada, 480]
Las cajas y las esferas te llevarán lejos, pero tarde o temprano el juego
quiere un dragón. Un *modelo* es una forma que ha construido otra persona en un
programa de modelado: cientos de triángulos, sus propios colores y superficies,
a veces un esqueleto y unos movimientos que lo acompañan, todo en un archivo.
Cargarlo son dos líneas. Conseguir que parezca algo es cuestión de luz, y la luz
es la mayor parte de esta lección.
No necesitas arte ni descargas. La primera vez que ejecutaste Singe,
desempaquetó una carpeta llamada `Singe` en tu carpeta de trabajo, y dentro hay
dos modelos: `DragonModel.glb`, el dragón de la pantalla de título del propio
motor, y `SingeText.glb`, las letras que hay debajo. Esta lección usa el dragón.
[#_the_scene_around_it]
=== La escena a su alrededor
Un modelo necesita un sitio donde apoyarse. Crea una carpeta, crea dentro
`dragon.singe` y empieza con un suelo y una cámara. Si has hecho la lección
veintidós, nada de esto es nuevo; si no, es una escena encendida, una losa plana
sobre la que apoyarse y un punto desde el que mirar.
[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
----
Ejecútalo con `Singe -R dragon`. Una losa gris, vista un poco desde arriba,
sobre un fondo azul oscuro. La losa mide veinticuatro unidades de lado y algo
menos de media unidad de grosor, y colocarla en `y = -0.2` pone su cara de
arriba exactamente en `y = 0`, lo que hace que sea fácil pensar en todo lo que
coloques después: el suelo es cero.
[#_loading_the_dragon]
=== Cargar el dragón
Dos líneas.
[source,lua]
----
local dragonModel = modelLoad("Singe/DragonModel.glb")
local dragon = modelInstance(dragonModel)
----
Ponlas después del suelo y guarda. Aparece un dragón, y es muchísimo más grande
de lo que cabe en la imagen -- estás mirando la mitad de su cuerpo.
[#_what_just_happened_19]
=== Qué acaba de pasar
Las dos líneas hacen trabajos distintos, y la diferencia importa.
`modelLoad` lee el archivo, le pasa las formas y sus superficies a la tarjeta
gráfica y lo deja todo listo. Es la llamada lenta. Hazla una vez, cuando empieza
el juego, y nunca en mitad de la partida.
`modelInstance` coloca una copia en la escena y te devuelve un nodo -- un nodo
normal, del mismo tipo que te da `nodeNew`, que colocas, giras, escalas y borras
con las llamadas que ya conoces. Instanciar es la llamada barata. Carga un
árbol, instáncialo cuarenta veces y tendrás un bosque, con una sola copia de las
formas en la tarjeta gráfica y cuarenta sitios donde se dibujan.
Lo que viene con el modelo es todo lo que había en el archivo: las formas, las
superficies y sus colores, cómo están colocadas las partes, el esqueleto si lo
hay y los movimientos si los hay. No construyes materiales para un modelo
cargado; ya ha traído los suyos.
[#_scale_and_the_first_thing_that_goes_wrong]
=== La escala, y lo primero que sale mal
El dragón mide unas siete unidades y media de alto. Tu cámara está nueve
unidades más atrás. Por eso llena la ventana.
[source,lua]
----
nodeSetScale(dragon, 0.5)
----
Añade eso debajo de la línea `modelInstance`. Ahora mide menos de cuatro
unidades de alto, se apoya en el suelo y cabe en el plano.
Esto no es un detalle; es el primer problema más común con un modelo, y tiene
dos caras. Un modelo construido en centímetros llega cien veces más grande de la
cuenta y ves una superficie enorme llenando la pantalla, lo que suele parecer
«mi escena está rota» más que «estoy dentro del cuello del dragón». Un modelo
construido en alguna otra unidad llega tan pequeño que es un punto, o invisible
contra el fondo, y das por hecho que el archivo no se ha cargado.
El arreglo es siempre el mismo: una escala en el nodo de la instancia. No en las
partes, ni en el programa de modelado si puedes evitarlo, sino un único
`nodeSetScale` en el nodo que te ha dado `modelInstance`, elegido probando un
número y mirando. `0.01` y `100` son respuestas perfectamente normales las dos.
Cuando no sepas lo grande que es algo, búscate una regla. Pon una caja de una
unidad a su lado:
[source,lua]
----
local ruler = nodeNew()
nodeSetMesh(ruler, meshBox(1, 1, 1), stone)
nodeSetPosition(ruler, 3, 0.5, 0)
----
Un objeto del tamaño de un cajón al lado del modelo te dice de un vistazo si el
modelo es un dragón o una montaña. Quita la regla cuando termines.
Mientras lo colocas, gíralo para que te mire:
[source,lua]
----
nodeSetRotation(dragon, 0, -60, 0)
----
El dragón se modeló mirando a lo largo de la X positiva, que es hacia la
derecha, así que empieza de perfil. Girarlo alrededor de Y lo vuelve hacia la
cámara. El «frente» de un modelo es la dirección que haya elegido quien lo
construyó, y para averiguarlo hay que girarlo y mirar.
[#_light]
=== Luz
Ejecuta lo que tienes ahora. El dragón está ahí, y es deprimente: una forma
plana y apagada, sin reflejos y sin sombra. Eso es porque la única luz de tu
escena es la ambiente, el débil resplandor que llega de todas partes y que fija
`sceneSetAmbient`.
Esta es la decepción más común del 3D, y le pasa a todo el mundo: el modelo se
ha cargado perfectamente y la escena está negra, o casi, porque no le da ninguna
luz. Antes de sospechar del archivo, pon una luz.
Hay tres tipos, y cada una es un nodo, así que las colocas y las apuntas con
`nodeSetPosition` y `nodeLookAt` como cualquier otra cosa.
[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` es un sol. Se trata como si estuviera infinitamente lejos,
así que solo cuenta la dirección en la que brilla y todo lo que hay en la escena
recibe la misma cantidad. Su posición está ahí para que `nodeLookAt` la apunte.
Esta es la luz que le da a una escena su forma principal, y la mayoría de las
escenas quieren exactamente una.
`lightSetIntensity` es el brillo. En una luz direccional, `1` lleva hasta el
blanco una superficie blanca que la tiene de frente, así que `1.8` es un día
luminoso. `lightSetColor` la tiñe: un blanco un poco cálido para el sol, un azul
frío para la luz de luna, y la sección Luz de la referencia para el resto.
`lightSetShadow(sun, true)` es lo que hace que el dragón se apoye en el suelo en
lugar de flotar encima. Las sombras están desactivadas por defecto porque cada
luz que proyecta sombras cuesta otra pasada por toda la escena en cada
fotograma. Actívala en la única luz que importa y déjala desactivada en las
demás; una máquina pequeña te lo agradecerá.
[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` es una bombilla. Está en un sitio, brilla en todas las direcciones
y se debilita con la distancia. Ese debilitamiento es la razón de que el número
sea `14` y no `1`: la intensidad de una luz puntual es su brillo en la propia
luz, y para cuando ha recorrido unas cuantas unidades no queda mucho. Una
bombilla que ilumina una habitación suele querer algo de unas decenas. Si una
luz puntual parece no hacer nada, casi siempre es por eso.
`lightSetRange` dice hasta dónde llega antes de apagarse del todo, lo que evita
que una lámpara de una habitación ilumine débilmente la siguiente, y no cuesta
nada ponerlo.
El tercer tipo es `LIGHT_SPOT`, un cono que apunta a lo largo de la Z negativa
del propio nodo, con `lightSetCone` para el ángulo del centro brillante y del
borde suave: una linterna, un faro, un reflector. Pueden brillar hasta ocho
luces a la vez, que es más de lo que necesitan la mayoría de las escenas. Si
quieres más, lo que quieres es menos.
Ejecútalo ahora. Un sol casi blanco desde la izquierda, un resplandor naranja
cálido desde la derecha, una sombra en la losa y un dragón con una parte de
delante y otra de detrás.
[#_materials_at_the_level_you_need_today]
=== Materiales, al nivel que necesitas hoy
El dragón ha traído sus propios materiales, y son tres: un metal oscuro, un
metal champán más claro y un cristal pálido en el que se ve un poco hacia
dentro. La mayoría de sus partes están hechas de más de uno. No tienes que tocar
nada de eso, y casi siempre no deberías.
Cuando sí quieras cambiar el aspecto de una parte de un modelo, creas un
material como en la lección veintidós y se lo pones a la parte:
[source,lua]
----
local red = materialNew()
materialSetColor(red, 220, 50, 40)
materialSetRoughness(red, 0.4)
nodeSetMaterial(nodeFind("crest", dragon), red)
----
`nodeFind(name, root)` busca en una instancia de modelo una parte por el nombre
que tenía en el archivo y te devuelve su nodo, o `nil` cuando no hay ningún
nombre así. Así es como llegas al interior de un modelo: una torreta, una rueda,
una mano de la que colgar una antorcha, o la cresta del dragón.
La cresta es una sola pieza de un solo material, así que su nodo lleva la forma
en sí y la línea de arriba la vuelve a pintar. Una parte construida con varios
materiales se guarda como un nodo con un hijo por pieza, y ponerle un material
al padre no cambia nada que se pueda ver. Cuando un `nodeSetMaterial` parece no
hacer nada, es por eso, y `nodeGetChildren` te enseñará lo que hay debajo.
Cuatro números describen la mayoría de las superficies. El color, con
`materialSetColor`. `materialSetRoughness`, de `0` para un espejo a `1` para la
tiza. `materialSetMetallic`, que es `0` para casi todo -- pintura, madera,
plástico, piel -- y `1` para el metal de verdad, y los valores intermedios
sirven de poco. Y `materialSetEmissive`, que hace que una superficie brille con
su propio color, para pantallas, lámparas y hierro al rojo, sin iluminar nada a
su alrededor.
Una cosa que debes saber del metal: una superficie metálica te muestra sus
reflejos en lugar de un color propio, así que en una escena sin nada que
reflejar se ve oscura y apagada. Por eso el metal oscuro del dragón necesita el
sol para cobrar vida. El arreglo de verdad es un cielo (`sceneSetSky`), que
envuelve la escena en un panorama que a la vez se ve detrás de todo y lo
ilumina; la sección «El aspecto del fotograma» de la referencia lo explica.
[#_movement_the_model_brings_with_it]
=== El movimiento que trae el modelo
Muchos modelos llevan animaciones: un paseo, una carrera, una pose de reposo,
una puerta que se abre. Se guardan en el archivo, y reproducir una es una sola
llamada sobre el nodo raíz de la instancia.
[source,lua]
----
animationPlay(dragon, "Walk", true)
----
El segundo argumento es el nombre del clip tal como lo guarda el archivo, o su
número. `true` significa repetirlo para siempre. Dos números opcionales más
fijan la velocidad y el fundido cruzado:
`animationPlay(hero, "Run", true, 1.2, 0.4)` reproduce la carrera una quinta
parte más rápido y pasa a ella fundiéndose durante cuatro décimas de segundo,
así que el paseo se convierte en carrera sin un salto brusco. `animationStop`,
`animationPause` y `animationIsPlaying` hacen lo que dicen sus nombres.
No puedes ejecutar esa línea todavía, y esta es la razón sincera: el dragón de
tu carpeta `Singe` no tiene ninguna animación. Es un modelo, no una actuación.
Pedir un clip que no existe detiene el juego con un error, así que nunca des
nada por hecho -- pregunta:
[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` te devuelve una tabla con los nombres de los clips del
archivo, en orden, así que `clips[1]` es el primero y `#clips` es cuántos
hay -- las llamadas de tablas de la lección seis, sobre una tabla que ha llenado
el motor. Ejecútalo y mira la consola: aparece el mensaje, porque esa cuenta es
cero.
Coge la costumbre de hacer esa comprobación con cualquier modelo que no hayas
hecho tú. Cuesta tres líneas y convierte «el juego murió al arrancar» en una
frase en el registro.
[#_moving_the_parts_yourself]
=== Mover las partes tú mismo
Un modelo sin animaciones no es una estatua. Sus partes son nodos, y los nodos
giran.
[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
----
Guárdalo. Las alas baten, el dragón gira despacio sobre sí mismo, y la sombra
del suelo bate con ellas.
`singeGetTicks` es el número de milisegundos desde que arrancó el motor, así que
sube sin parar para siempre. `math.sin` toma ese número que sube y devuelve un
valor que se desliza suavemente de `-1` a `1` y vuelta, un aleteo completo más o
menos cada segundo y medio con este divisor. Multiplicar por `22` lo convierte
en un ángulo de veintidós grados hacia cada lado. Dividir por un número mayor
hace el aleteo más lento; multiplicar por un número mayor lo hace más amplio.
Esa línea es la animación más barata que existe, y vale la pena llevarla siempre
en el bolsillo.
Las alas giran alrededor de su propio eje X, y giran alrededor del hombro y no
del centro del animal, porque quien construyó el archivo puso el nodo de cada
parte donde va la articulación. Un modelo que no se construyó así hará girar sus
partes alrededor del centro del modelo, y el arreglo es mover el nodo de la
parte, cosa que explica la sección Nodo de la referencia.
Fíjate en que el giro de todo el dragón no interfiere con el giro de las alas.
Las partes cuelgan de la raíz de la instancia, así que girar la raíz se lo lleva
todo consigo, y la rotación propia de cada ala va encima de eso. Ese montaje --
una cosa dentro de una cosa dentro de una cosa, cada una moviéndose en el mundo
de su padre -- es para lo que sirve un árbol de escena.
[#_your_own_models]
=== Tus propios modelos
Cuando hagas los tuyos, expórtalos desde Blender -- o Maya, o 3ds Max, o
cualquier otro programa moderno -- como *glTF 2.0 binario*, que es un archivo
`.glb`. Es la opción por defecto de Blender y es el único formato de modelo que
lee Singe.
La única regla es que el archivo tiene que ser autosuficiente: las formas, las
imágenes que llevan encima y los movimientos, todo dentro del único `.glb`. Un
archivo que hace referencia a un `.bin` o a un `.png` que está a su lado se
rechaza, con un mensaje que nombra lo que quería. El exportador de Blender hace
lo correcto con sus ajustes por defecto.
No todo lo que puedes construir sobrevive al viaje. La sección «Modelos y
animación» de la referencia, en el capítulo «Escenas 3D», es la lista: lo que se lee,
lo que se ignora sin avisar y los límites (un esqueleto puede tener hasta ciento
veintiocho articulaciones, por ejemplo). Léela una vez antes de pasarte un fin
de semana con un modelo, no después.
[#_try_it_23]
=== Pruébalo
. *Apaga las luces.* Comenta todo el bloque del `sun` poniendo `--` al
principio de cada línea, y ejecútalo. Luego haz lo mismo con
`sceneSetAmbient`. Recuerda cómo se ve, porque un día te pasará sin querer.
. *Mueve el sol.* Cambia `nodeLookAt(sun, 0, 1.5, 0)` por
`nodeLookAt(sun, 0, 20, 0)` para que el sol apunte por encima de la cabeza
del dragón. Mira cómo se van la sombra y los reflejos. Apuntar una luz no es
decoración.
. *Calienta la antorcha.* Sube la intensidad de la luz puntual de `14` a `60` y
baja su alcance a `4`. Luego muévela a `(0, 6, 0)`, encima del dragón. Una
luz puntual es una bombilla, y tú eliges dónde cuelga la bombilla.
. *Instáncialo dos veces.* Llama a `modelInstance(dragonModel)` una segunda
vez, guárdalo en otra variable, escálalo y ponlo en `(-4, 0, -3)`. Una carga,
dos dragones. Luego prueba en cambio a cargar el modelo dos veces y entiende
por qué no deberías.
. *Busca otra parte.* Las partes del dragón se llaman `head`, `jaw`, `neck`,
`tail`, `body`, `crest`, `hornL`, `hornR`, `wingL`, `wingR`, `foreLegL`,
`foreLegR`, `hindLegL` y `hindLegR`. Haz que la cola se balancee con su propio `math.sin`
a una velocidad distinta de la de las alas. Luego prueba
`nodeFind("wings", dragon)` y mira lo que hace un nombre equivocado cuando le
pasas el `nil` directamente a `nodeSetRotation`.
[#_break_it_on_purpose_22]
=== Rómpelo a propósito
Cambia el nombre del archivo a `Singe/Dragonmodel.glb` -- una sola mayúscula de
diferencia -- y ejecútalo. Singe se detiene antes de que arranque el juego:
----
13:modelLoad: Unable to read Singe/Dragonmodel.glb.
----
La línea, la llamada y la queja, en ese orden. «Unable to read» significa que el
motor ha buscado y no ha encontrado nada, y eso casi siempre es una de tres
cosas: el nombre está mal escrito, las mayúsculas están mal o el archivo no está
donde dijiste. En Windows se te habrían perdonado las mayúsculas, y entonces tu
juego fallaría a todo el que use Linux, que es un fallo peor que este.
Escribe los nombres exactamente.
El mismo mensaje con un nombre de archivo correcto significa que el archivo no
está donde lo buscó el motor. La carpeta `Singe` se crea en la carpeta *desde* la que
ejecutas el motor, así que si arrancaste Singe desde otro sitio,
`Singe/DragonModel.glb` también apunta a otro sitio.
[#_what_you_learned_23]
=== Lo que has aprendido
* `modelLoad` lee un `.glb` una vez; `modelInstance` coloca copias de él tantas
veces como quieras.
* La raíz de una instancia es un nodo normal: colócala, gírala, escálala y
bórrala como cualquier otro.
* Un modelo trae consigo sus propias formas y materiales.
* Un modelo llega al tamaño que usó su autor, y por eso un `nodeSetScale` en la
instancia forma parte de colocarlo.
* Una escena sin luz está negra o casi, y es lo primero que hay que comprobar
cuando un modelo se ve mal.
* Una luz direccional es un sol, una luz puntual es una bombilla que se debilita
con la distancia y necesita una intensidad mucho mayor, y un foco es un cono.
* Las sombras están desactivadas hasta que `lightSetShadow` las activa, y cada
una tiene su coste.
* `nodeFind` llega a una parte de un modelo por su nombre en el archivo.
* `modelGetAnimations` dice qué puede reproducir un modelo, y pedir un clip que
no existe es un error.
* Un modelo sin animaciones se puede mover igualmente, parte a parte.
[#_next_time_23]
=== La próxima vez
Todo lo que hay en esta escena se mueve porque tú se lo has dicho, fotograma a
fotograma. La próxima lección le pasa ese trabajo a un motor de física: cajas
que caen, ruedan, se apilan y se tiran unas a otras sin que escribas ni una
línea de aritmética -- y la pregunta más difícil de cuándo deberías dejarle. Esa
es la lección veinticuatro.