452 lines
19 KiB
Text
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.
|