Cámara y 3D
Encuadres, zoom, seguimiento y cámaras secundarias en 2D; primitivas, luces y cámara en perspectiva en 3D
En esta guía aprenderás a dirigir la mirada del espectador: acercarte a un detalle, volver al plano general, seguir a un objeto en movimiento, sacudir la imagen y mostrar un detalle ampliado con una cámara secundaria. Al final verás cómo montar una escena 3D sencilla y moverte alrededor de ella.
from gaanim import CYAN, GREEN, WHITE, Easing, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
left = scene.geometry.circle(0.8).fill(CYAN).move_to(-4, 0)
right = scene.geometry.square(1.4).fill(GREEN).move_to(4, 0)
label = scene.text("Detalle", role="caption").fill(WHITE).move_to(4, -1.2)
scene.play([
left.animate.grow_from_center().duration(0.4),
right.animate.grow_from_center().duration(0.4),
label.animate.fade_in().duration(0.4),
])
scene.camera.save("general")
scene.play([scene.camera.animate.frame_to([right, label], margin=0.5).duration(1.0).easing(Easing.SMOOTH)])
scene.play([scene.camera.animate.restore("general").duration(0.8)])
scene.play([scene.camera.animate.shake(trauma=0.6, seed=1)])
scene.render()
La cámara 2D
scene.camera controla qué parte de la escena se ve. Trabaja en las mismas unidades lógicas que los objetos (un fotograma de 16 × 9), así que un zoom de 2 muestra la mitad de ancho y de alto.
Cada operación existe en dos formas:
scene.camera.zoom_to(2)aplica el cambio al instante, en el cursor actual.scene.camera.animate.zoom_to(2)devuelve una animación que pasas ascene.play([...])con su duración y su easing.
Mover, acercar y girar
scene.play([scene.camera.animate.pan_to(3, 1).duration(0.8)])
scene.play([scene.camera.animate.zoom_to(2.5).duration(1.0)])
scene.play([scene.camera.animate.rotate_to(0.2).duration(0.6)])
scene.play([scene.camera.animate.reset().duration(0.8)])pan_to(x, y)centra la vista en un punto; también acepta un drawable.zoom_to(z)acerca con valores mayores que1. Por defecto interpola de forma exponencial para que un zoom grande se perciba a velocidad constante;interpolation="linear"recupera la interpolación lineal.rotate_to(ángulo)gira la vista, en radianes.reset()vuelve al encuadre inicial.
Encuadrar objetos
frame_to(objetos, margin=) calcula el paneo y el zoom necesarios para que los objetos quepan en pantalla con un margen. Es la forma más cómoda de acercarse a un detalle sin calcular coordenadas:
scene.play([scene.camera.animate.frame_to([target], margin=0.6).duration(1.0)])Con dynamic=True el encuadre se recalcula mientras los objetos se mueven.
Guardar y recuperar encuadres
scene.camera.save("nombre") guarda el encuadre actual y scene.camera.animate.restore("nombre") vuelve a él. Para definir un encuadre sin aplicarlo, usa scene.camera.state_2d(...) con center, zoom y rotation, y anímalo con scene.camera.animate.to(estado):
overview = scene.camera.state_2d(center=(0, 0), zoom=1.0)
detail = scene.camera.state_2d(center=(3, 1), zoom=3.0)
scene.play([scene.camera.animate.to(detail).duration(1.0)])
scene.play([scene.camera.animate.to(overview).duration(1.0)])Seguir a un objeto
scene.camera.animate.follow(objeto, lag=) mantiene el objeto en el centro mientras dura la animación, y al terminar la cámara se queda donde lo dejó. lag añade un retraso suave, en segundos:
from gaanim import GOLD, WHITE, Easing, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
for x in range(-7, 8):
for y in range(-4, 5):
scene.geometry.dot(0.05).fill(WHITE).move_to(x, y)
car = scene.geometry.rect(0.8, 0.4).fill(GOLD).move_to(-6, -2)
scene.play([
car.animate.move_to(6, 2).duration(2.0).easing(Easing.SMOOTH),
scene.camera.animate.follow(car, lag=0.2).duration(2.0),
])
scene.render()
Para un vínculo que dure varias animaciones, scene.camera.bind_2d(center=, conecta la cámara a extremos o parámetros y devuelve un
zoom=, rotation=)CameraConstraint que puedes desactivar con disable():
zoom = scene.viz.parameter(1.0)
constraint = scene.camera.bind_2d(center=car, zoom=zoom)
scene.play([zoom.animate.set(2.0).duration(0.8)])
constraint.disable()Consulta Reactividad para crear esos parámetros y extremos.
Sacudir la imagen
shake() produce un temblor determinista que termina en reposo. trauma (entre 0 y 1) controla la intensidad, decay cuánto tarda en apagarse y seed fija el patrón:
scene.play([scene.camera.animate.shake(trauma=0.8, decay=1.5, seed=3)])Cámaras secundarias
scene.camera mueve toda la imagen. Para enseñar un detalle ampliado sin perder el plano general, convierte una figura en una pantalla con pantalla.camera_view(marco): la pantalla muestra lo que ve una segunda cámara que encuadra marco. Es el picture-in-picture de un vídeo o la lupa sobre un mapa.
from gaanim import CYAN, GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
for i in range(60):
scene.geometry.dot(0.03).fill(CYAN if i % 2 else GOLD).move_to(-6.5 + 0.1 * i, 0.3 * (i % 5) - 0.6)
scene.text("detalle", role="caption").scale_to(0.2).move_to(-4.5, -1.2)
scene.text("letra pequeña bajo la lupa", role="caption").scale_to(0.3).move_to(-4, 1.8)
marco = scene.geometry.rect(1.6, 0.9).no_fill().stroke(GOLD, 0.03).move_to(-4.5, -0.9)
pantalla = scene.geometry.rounded_rect(6.4, 3.6, 0.2).stroke(GOLD, 0.05).move_to(3.5, 0)
pantalla.camera_view(marco)
lupa = scene.geometry.circle(0.7).stroke(WHITE, 0.04).move_to(-6, 1.8)
foco = scene.geometry.circle(0.25).no_fill().no_stroke().move_to(-6, 1.8)
lupa.camera_view(foco)
scene.play([
marco.animate.move_to(-2, 0).scale_to(0.5).duration(1.5),
lupa.animate.move_to(-2, 1.8).duration(1.5),
foco.animate.move_to(-2, 1.8).duration(1.5),
])
scene.render()
- El marco es la cámara: moverlo desplaza la vista, reducirlo acerca y girarlo gira la vista en sentido contrario. Se anima con
animatecomo cualquier objeto. - El aumento es el tamaño de la pantalla entre el del marco: una pantalla de 6,4 de ancho con un marco de 1,6 amplía cuatro veces.
fit="cover"llena la pantalla aunque las proporciones no coincidan yfit="stretch"deforma el marco hasta encajarlo. - La pantalla dibuja su relleno, encima la vista recortada a su contorno y encima su trazo. Cualquier figura cerrada sirve: un círculo hace de lupa.
- La vista no muestra la pantalla ni el marco. Oculta más objetos con
exclude=[...]; si el marco solo debe hacer de cámara, quítale el trazo. - El fondo de la vista es el de la escena. Cámbialo con
background=(un color o unBrush), o pasabackground=Nonepara que se vea el relleno de la pantalla. - La cámara ve la escena aunque la región quede fuera de la cámara principal, así que puedes mostrar a la vez dos zonas alejadas.
Zoom, paneo y pop-out
camera_view devuelve un CameraView con el mismo vocabulario que scene.camera: pan_to, zoom_to y rotate_to, al instante o con animate. Con zoom= la vista tiene su propio aumento y el tamaño del marco deja de contar; sin marco, la cámara mira a center a través de un marco invisible. animate.zoom_to cambia ese zoom a velocidad percibida constante, como el de la cámara principal.
animate.pop_out() hace brotar la pantalla de la región que ve su cámara: empieza encogida sobre esa región, mostrando la escena a tamaño real, y crece hasta su sitio; si es su entrada, hasta entonces está oculta. animate.pop_in() la devuelve y la oculta al llegar.
vista = scene.geometry.rounded_rect(5, 2.8, 0.2).stroke(WHITE, 0.05).move_to(-3.5, -2.3)
vista = vista.camera_view(center=(-4.5, -0.9), zoom=3)
scene.play([vista.animate.pop_out().duration(0.8)])
scene.play([vista.animate.pan_to(-1, 0).duration(1.0), vista.animate.zoom_to(6).duration(1.0)])
scene.play([vista.animate.pop_in().duration(0.6)])Insets con conectores
scene.camera.inset(objetivo, zoom=, at=) monta de una vez la pantalla, un marco sobre el objetivo que recuadra lo que muestra y dos conectores entre ambos. at coloca la pantalla en una esquina o un borde del fotograma, o en un punto. Con follow=True el marco persigue a un objeto en movimiento, y view.zoom es un valor reactivo que puedes mostrar con un readout.
from gaanim import CYAN, Anchor, Easing, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
for x in range(-7, 8):
for y in range(-4, 2):
scene.geometry.dot(0.04).fill("#334155").move_to(x, y)
bola = scene.geometry.circle(0.12).fill(CYAN).move_to(-6, -3)
view = scene.camera.inset(bola, zoom=5, at=Anchor.TOP_RIGHT, follow=True)
scene.viz.readout(view.zoom, format=".0f", prefix="×").move_to(4.5, 0.4)
scene.play([view.animate.pop_out().duration(0.8)])
scene.play([bola.animate.move_to(5, -2).duration(2.0).easing(Easing.SMOOTH)])
scene.play([view.animate.zoom_to(2).duration(1.0)])
scene.render()
Con fixed=True la pantalla se queda fija en la imagen aunque scene.camera se desplace o acerque, como cualquier objeto con .hud().
Capas: la lupa de rayos X
drawable.view_layer("nombre") saca un objeto de la cámara principal: solo lo muestran las vistas que listan esa capa en layers=. Así una lupa puede revelar lo que hay debajo de una superficie.
from gaanim import GOLD, WHITE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
scene.geometry.rounded_rect(9, 4, 0.6).fill("#475569").no_stroke()
for x in (-3, -1, 1, 3):
scene.geometry.rect(0.3, 3).fill(GOLD).no_stroke().move_to(x, 0).view_layer("rayos_x")
lupa = scene.geometry.circle(1.3).stroke(WHITE, 0.06).move_to(-4, 0)
vista = lupa.camera_view(center=(-4, 0), zoom=1.2, layers=["rayos_x"])
scene.play([
lupa.animate.move_to(4, 0).duration(3.0),
vista.animate.pan_to(4, 0).duration(3.0),
])
scene.render()
Escenas 3D
Gaanim puede mezclar objetos 3D con el resto de la escena. Empieza con una composición estática: luz, objetos y una cámara que los vea bien. Cuando sea legible, anima la cámara.
from gaanim import CYAN, GOLD, Material3D, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
scene.geometry.lighting_3d("studio", intensity=1.0)
cube = scene.geometry.cube(2.0, material=Material3D.matte(CYAN)).move_to_3d(-1.8, 0, 0)
sphere = scene.geometry.sphere(1.1, material=Material3D.metal(GOLD)).move_to_3d(1.8, 0, 0)
scene.camera.perspective(fov_y=0.7)
scene.camera.look_at(eye=(0, 3, 10), target=(0, 0, 0))
scene.play([cube.animate.create().duration(0.5), sphere.animate.create().duration(0.5)])
scene.play([scene.camera.animate.orbit(delta_yaw=0.8, delta_pitch=0.3).duration(1.2)])
scene.play([scene.camera.animate.dolly(0.8).duration(0.6)])
scene.render()Objetos, materiales y luz
scene.geometrycrea primitivas 3D:cube,sphere,cylinder,coneyplane.Material3D.matte(color),Material3D.metal(color)yMaterial3D.emissive(color, strength)son puntos de partida; el constructorMaterial3D(color, roughness=, metallic=)permite ajustarlos.scene.geometry.lighting_3d("studio")ilumina las primitivas con material al estilo de Gaanim: luz del cielo y del suelo, que aclara las caras que miran arriba y oscurece las que miran abajo; una luz principal suave desde arriba a la izquierda, y un leve brillo en los bordes de la silueta. El color del material se mantiene legible: una cara iluminada muestra más o menos su color, y una en sombra, la mitad. Los materiales pulidos (poca rugosidad) tienen un reflejo blanco y los metálicos reflejan el cielo.scene.geometry.light_3d()añade una luz puntual (o"directional", como el sol) que se coloca conmove_to_3dy se anima como cualquier objeto. Mientras hay luces, sustituyen a la luz principal fija, cada una en su color e intensidad;fade_inyfade_outla encienden y la apagan. Conlighting_3d("none")solo alumbran ellas."none"deja solo la emisión del material.- Las caras planas se pintan de un solo color y las curvas con degradados suaves.
- Los drawables tienen versiones 3D de sus transformaciones:
move_to_3d,shift_by_3d,rotate_by_3d(eje, radianes)yscale_by_3d, tanto inmediatas como bajo.animate.
Los ejes son los de la escena 2D: x hacia la derecha, y hacia arriba y z hacia el espectador.
La cámara en perspectiva
perspective(fov_y)activa la proyección en perspectiva;fov_yes el campo de visión vertical en radianes.look_at(eye=, target=, up=)coloca la cámara eneyemirando atarget.animate.orbit(delta_yaw=, delta_pitch=)gira alrededor del objetivo.animate.dolly(factor)se acerca (menor que1) o se aleja (mayor que1).orthographic()vuelve a la proyección ortográfica yreset()al encuadre 2D inicial.
Ejes y superficies 3D
scene.viz.cartesian_3d crea unos ejes 3D sobre los que puedes dibujar superficies, curvas paramétricas y campos. Una superficie puede depender de parámetros, como cualquier objeto reactivo:
import math
from gaanim import Axis, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
space = scene.viz.cartesian_3d(
Axis.linear(-3, 3).ticks(1),
Axis.linear(-3, 3).ticks(1),
Axis.linear(-1, 1).ticks(0.5),
size=(6, 6, 3),
)
phase = scene.viz.parameter(0.0)
surface = space.surface(lambda x, y, p: math.sin(math.hypot(x, y) - p), inputs=[phase])
scene.camera.perspective(fov_y=0.8)
scene.camera.look_at(eye=(7, -9, 6), target=(0, 0, 0), up=(0, 0, 1))
scene.play([space.animate.create().duration(0.6), surface.animate.fade_in().duration(0.6)])
scene.play([phase.animate.set(2 * math.pi).duration(2.0)])
scene.render()La coordenada z de los datos apunta hacia el espectador, así que para ver la superficie como un relieve se usa up=(0, 0, 1) en look_at.
Cómo se dibuja el 3D
Gaanim dibuja el 3D con Vello, igual que el 2D: proyecta las mallas de triángulos y las líneas 3D a través de la cámara de la escena, las ordena de atrás hacia delante y calcula la luz en la CPU. De ahí salen sus límites:
- Las primitivas con
Material3Dreciben luz y ocultan sus caras traseras. Las mallas sin material (superficies, gráficos 3D) no reciben luz y se ven por las dos caras; las líneas 3D tampoco reciben luz y miden 1,5 px. - No hay sombras:
lighting_3d(shadows=True)se acepta, pero ningún objeto proyecta sombra. - No hay búfer de profundidad: los triángulos se ordenan por profundidad, así que las superficies que se cruzan pueden mostrar errores de orden.
- Todo el contenido 3D se dibuja debajo del 2D: etiquetas, textos y HUD quedan siempre encima.
Como es dibujo normal de Vello, el 3D aparece en las exportaciones, las capturas, los paquetes .gaanim y las vistas previas de Presenter View, y admite desenfoque de movimiento y postprocesado.
Consejos
- Valida primero una escena quieta; añade el movimiento de cámara al final.
- Los objetos 3D usan las mismas unidades lógicas que el resto de la escena; mide tamaños y distancias de cámara con la misma escala.
Las firmas completas de la cámara están en la referencia de la escena.