aboutsummaryrefslogtreecommitdiffstats
path: root/blender/caos.py
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 20:50:41 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 20:50:41 -0300
commitfafaebb051907a848a9406f9da19669c81a83a3b (patch)
treec30ea26e6b549e5523af2bae5c39569e9a946b12 /blender/caos.py
parent59355909f2de9236af8168a26c70bcf6caa3b285 (diff)
download100cia-videos-fafaebb051907a848a9406f9da19669c81a83a3b.tar.gz
100cia-videos-fafaebb051907a848a9406f9da19669c81a83a3b.zip
Translate code, comments and logs to English; English README; configurable paths and env varsHEADmain
Identifiers, docstrings, comments and console messages are now in English. Narration, subtitles and on-screen text stay in Spanish (they are the video content). The Blender <-> Godot physics protocol uses English keys and body prefixes chosen to keep the original creation order, so cached simulations and renders stay bit-identical. The old Spanish environment variable names are still accepted.
Diffstat (limited to 'blender/caos.py')
-rw-r--r--blender/caos.py228
1 files changed, 114 insertions, 114 deletions
diff --git a/blender/caos.py b/blender/caos.py
index 99ef9db..ca45b07 100644
--- a/blender/caos.py
+++ b/blender/caos.py
@@ -1,32 +1,32 @@
# -*- coding: utf-8 -*-
-"""CAOS - tres pendulos dobles con 1 mm de diferencia inicial.
+"""CAOS - three double pendulums 1 mm apart at the start.
-La fisica es un RK4 sobre las ecuaciones exactas del pendulo doble, en unidades
-SI (L1 = L2 = 1 m), integrada antes de renderizar y muestreada por frame. El
-milimetro del guion es literal: 0,001 rad sobre una varilla de 1 m.
+The physics is RK4 on the exact double-pendulum equations, in SI units
+(L1 = L2 = 1 m), integrated before rendering and sampled per frame. The
+script's millimeter is literal: 0.001 rad on a 1 m rod.
"""
import math, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bpy
from base import *
-NOMBRE = "caos"
+NAME_KEY = "caos"
G, L1, L2, M1, M2 = 9.81, 1.0, 1.0, 1.0, 1.0
-TH0 = 2.4 # 138 grados: regimen caotico, separacion visible a los ~2,5 s
-DELTA = 0.001 # radianes = 1 mm en la punta de la primera varilla
-ESC = 0.80 # escala de dibujo (la fisica sigue en metros)
-ALTO = 6.0 # metros visibles de alto en el encuadre
-PZ_A, PZ_B = 0.47, 1.11 # altura del pivote antes / despues del beat 8
-K_B = 0.66 # el rig se achica para dejar lugar al grafico
-# banda util medida sobre el overlay: el chip llega a z=+2,27 y la caja de
-# subtitulos arranca en z=-1,09 (tres lineas) / -1,25 (dos lineas)
+TH0 = 2.4 # 138 degrees: chaotic regime, visible divergence at ~2.5 s
+DELTA = 0.001 # radians = 1 mm at the tip of the first rod
+SCALE = 0.80 # drawing scale (physics stays in meters)
+FRAME_H = 6.0 # meters of height visible in the frame
+PZ_A, PZ_B = 0.47, 1.11 # pivot height before / after beat 8
+K_B = 0.66 # the rig shrinks to make room for the graph
+# usable band measured on the overlay: the chip reaches z=+2.27 and the
+# subtitle box starts at z=-1.09 (three lines) / -1.25 (two lines)
GX0, GZ0, GW, GH = -1.45, -0.92, 2.90, 0.80
COLS = ["ambar", "rosa", "celeste"]
-ESTELA = 34 # frames de cola
-BEAT_SUELTA = 4 # se sueltan al empezar este beat
+ESTELA = 34 # tail frames
+BEAT_RELEASE = 4 # released when this beat starts
-# --- integrador ---------------------------------------------------------------
+# --- integrator ---------------------------------------------------------------
def deriv(s):
t1, t2, w1, w2 = s
d = t1 - t2
@@ -46,174 +46,174 @@ def rk4(s, h):
return tuple(s[i] + h / 6 * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) for i in range(4))
-def articulaciones(s):
- """(codo, punta) en metros, relativo al pivote."""
+def joints(s):
+ """(elbow, tip) in meters, relative to the pivot."""
t1, t2, _, _ = s
cx, cz = L1 * math.sin(t1), -L1 * math.cos(t1)
return (cx, cz), (cx + L2 * math.sin(t2), cz - L2 * math.cos(t2))
-def integrar(n_frames, sub=20):
- """Estados de los tres pendulos, muestreados por frame."""
- est = [(TH0 + k * DELTA, TH0, 0.0, 0.0) for k in range(3)]
+def integrate(n_frames, sub=20):
+ """States of the three pendulums, sampled per frame."""
+ estimate = [(TH0 + k * DELTA, TH0, 0.0, 0.0) for k in range(3)]
h = 1.0 / (FPS * sub)
- tray = [[articulaciones(s) for s in est]]
+ tray = [[joints(s) for s in estimate]]
for _ in range(n_frames):
for _ in range(sub):
- est = [rk4(s, h) for s in est]
- tray.append([articulaciones(s) for s in est])
+ estimate = [rk4(s, h) for s in estimate]
+ tray.append([joints(s) for s in estimate])
return tray
-# --- escena -------------------------------------------------------------------
-def construir(T):
- sc = escena()
- lente = 70.0
- dist = lente / 36.0 * ALTO # distancia para ver ALTO metros de alto
- # la camara se centra en z=0: asi las coordenadas coinciden con el cuadro
- # y se puede ubicar todo respecto del chip y de los subtitulos
- cam = camara((0.0, -dist, 0.0), (0.0, 0.0, 0.0), lente=lente)
+# --- scene -------------------------------------------------------------------
+def build_scene(T):
+ sc = scene_setup()
+ lens = 70.0
+ dist = lens / 36.0 * FRAME_H # distance to see FRAME_H meters of height
+ # the camera is centred on z=0: that way coordinates match the frame and
+ # everything can be placed relative to the chip and the subtitles
+ cam = camera_obj((0.0, -dist, 0.0), (0.0, 0.0, 0.0), lens=lens)
cam.data.sensor_fit = 'VERTICAL'
cam.data.sensor_height = 36.0
- luz("key", 'AREA', (-3.6, -5.2, 4.6), 900, "blanco", tam=5.0, mira=(0, 0, PZ_A))
- luz("fill", 'AREA', (4.2, -4.6, -0.6), 300, "celeste", tam=5.0, mira=(0, 0, PZ_A))
- luz("rim", 'AREA', (0.8, 4.6, 2.6), 520, "blanco", tam=4.0, mira=(0, 0, PZ_A))
+ light_obj("key", 'AREA', (-3.6, -5.2, 4.6), 900, "blanco", size_u=5.0, sight=(0, 0, PZ_A))
+ light_obj("fill", 'AREA', (4.2, -4.6, -0.6), 300, "celeste", size_u=5.0, sight=(0, 0, PZ_A))
+ light_obj("rim", 'AREA', (0.8, 4.6, 2.6), 520, "blanco", size_u=4.0, sight=(0, 0, PZ_A))
- # cubo del pivote: un disco metalico mirando a camara, sin soporte que tape
- m_hub = material("hub", "riel", rug=0.35, metal=0.9)
- hub = cilindro("hub", 0.105, 0.10, m_hub)
+ # pivot hub: a metal disc facing the camera, with no support blocking the view
+ m_hub = material("hub", "riel", rough=0.35, metal=0.9)
+ hub = cylinder("hub", 0.105, 0.10, m_hub)
hub.rotation_euler = (math.pi / 2, 0, 0)
- aro = cilindro("aro", 0.145, 0.05, material("aro", "gris", rug=0.25, metal=1.0, emis=0.5))
- aro.rotation_euler = (math.pi / 2, 0, 0)
+ hoop = cylinder("aro", 0.145, 0.05, material("aro", "gris", rough=0.25, metal=1.0, emit=0.5))
+ hoop.rotation_euler = (math.pi / 2, 0, 0)
pend = []
for k, c in enumerate(COLS):
- m = material(f"p{k}", c, rug=0.28, metal=0.55, emis=0.25)
- m_bola = material(f"b{k}", c, rug=0.18, metal=0.2, emis=0.9)
+ m = material(f"p{k}", c, rough=0.28, metal=0.55, emit=0.25)
+ m_bola = material(f"b{k}", c, rough=0.18, metal=0.2, emit=0.9)
pend.append({
"y": -0.035 * (k - 1), "col": c,
- "v1": cilindro(f"v1_{k}", 0.034, 1.0, m),
- "v2": cilindro(f"v2_{k}", 0.029, 1.0, m),
- "codo": esfera(f"codo_{k}", 0.055, m_bola),
- "punta": esfera(f"punta_{k}", 0.090, m_bola),
- "estela": curva_poly(f"estela_{k}", [[(0, 0, 0)] * ESTELA], grosor=0.032,
+ "v1": cylinder(f"v1_{k}", 0.034, 1.0, m),
+ "v2": cylinder(f"v2_{k}", 0.029, 1.0, m),
+ "codo": sphere(f"codo_{k}", 0.055, m_bola),
+ "punta": sphere(f"punta_{k}", 0.090, m_bola),
+ "estela": curve_poly(f"estela_{k}", [[(0, 0, 0)] * ESTELA], thickness_px=0.032,
radios=[[0.0] * ESTELA],
- mat=material(f"e{k}", c, rug=0.5, emis=2.4)),
+ mat=material(f"e{k}", c, rough=0.5, emit=2.4)),
})
- leyenda = []
+ legend = []
for k, (c, txt) in enumerate(zip(COLS, ("arranca +0 mm", "arranca +1 mm", "arranca +2 mm"))):
- t = texto(txt, tam=0.23, color=c, align='LEFT')
+ t = txt_m(txt, size_u=0.23, color=c, align='LEFT')
t.location = (-1.25, -0.6, -0.28 - 0.38 * k)
- p = esfera(f"pt_{k}", 0.075, material(f"pm{k}", c, emis=2.2))
+ p = sphere(f"pt_{k}", 0.075, material(f"pm{k}", c, emit=2.2))
p.location = (-1.42, -0.6, -0.28 - 0.38 * k)
- leyenda.append((t, p))
-
- m_ejes = material("ejes", "gris", rug=0.6, emis=0.9)
- graf = {
- "x": cilindro("gx", 0.014, GW, m_ejes),
- "y": cilindro("gy", 0.014, GH, m_ejes),
- "curva": curva_poly("gcurva", [[(0, 0, 0)] * 2], grosor=0.030,
- radios=[[1.0] * 2], mat=material("gc", "rosa", emis=2.8)),
- "arr": texto("2 metros", tam=0.20, color="gris", align='LEFT'),
- "aba": texto("1 mm", tam=0.20, color="gris", align='RIGHT'),
+ legend.append((t, p))
+
+ m_axes = material("ejes", "gris", rough=0.6, emit=0.9)
+ graph = {
+ "x": cylinder("gx", 0.014, GW, m_axes),
+ "y": cylinder("gy", 0.014, GH, m_axes),
+ "curva": curve_poly("gcurva", [[(0, 0, 0)] * 2], thickness_px=0.030,
+ radios=[[1.0] * 2], mat=material("gc", "rosa", emit=2.8)),
+ "arr": txt_m("2 metros", size_u=0.20, color="gris", align='LEFT'),
+ "aba": txt_m("1 mm", size_u=0.20, color="gris", align='RIGHT'),
}
- graf["x"].rotation_euler = (0, math.pi / 2, 0)
- graf["x"].location = (GX0 + GW / 2, 0.25, GZ0)
- graf["y"].location = (GX0, 0.25, GZ0 + GH / 2)
- # las etiquetas van donde la curva no pasa: arriba a la izquierda y abajo a
- # la derecha (la curva sale de abajo-izquierda y termina arriba-derecha)
- graf["arr"].location = (GX0 + 0.12, 0.25, GZ0 + GH + 0.02)
- graf["aba"].location = (GX0 + GW - 0.10, 0.25, GZ0 + 0.13)
- return dict(cam=cam, pend=pend, leyenda=leyenda, graf=graf, hub=hub, aro=aro)
+ graph["x"].rotation_euler = (0, math.pi / 2, 0)
+ graph["x"].location = (GX0 + GW / 2, 0.25, GZ0)
+ graph["y"].location = (GX0, 0.25, GZ0 + GH / 2)
+ # the labels go where the curve does not pass: top left and bottom right
+ # (the curve starts bottom-left and ends top-right)
+ graph["arr"].location = (GX0 + 0.12, 0.25, GZ0 + GH + 0.02)
+ graph["aba"].location = (GX0 + GW - 0.10, 0.25, GZ0 + 0.13)
+ return dict(cam=cam, pend=pend, legend=legend, graph=graph, hub=hub, hoop=hoop)
def main():
- T = Tiempo(NOMBRE)
- f_suelta = T.rango(BEAT_SUELTA)[0]
- n_sim = T.n_frames - f_suelta + 2
- print(f"[{NOMBRE}] integrando {n_sim} frames de fisica...", flush=True)
- tray = integrar(n_sim)
- obj = construir(T)
- pend, graf = obj["pend"], obj["graf"]
-
- # separacion entre la punta 0 y la punta 2, en metros, por frame
+ T = Timeline(NAME_KEY)
+ f_release = T.span(BEAT_RELEASE)[0]
+ n_sim = T.n_frames - f_release + 2
+ print(f"[{NAME_KEY}] integrating {n_sim} physics frames...", flush=True)
+ tray = integrate(n_sim)
+ obj = build_scene(T)
+ pend, graph = obj["pend"], obj["graph"]
+
+ # separation between tip 0 and tip 2, in meters, per frame
sep = [math.dist(p[0][1], p[2][1]) for p in tray]
LMIN, LMAX = math.log10(0.001), math.log10(2.0)
- # separacion maxima alcanzada: monotona, se lee de un vistazo
+ # maximum separation reached: monotonic, it reads at a glance
sep, mx = [], 0.0
for p in tray:
mx = max(mx, math.dist(p[0][1], p[2][1]))
sep.append(mx)
LMIN, LMAX = math.log10(0.001), math.log10(2.0)
- def actualizar(f):
- idx = max(0, min(f - f_suelta, len(tray) - 1))
- # el rig se achica y sube en el beat 8 para dejarle lugar al grafico
+ def refresh(f):
+ idx = max(0, min(f - f_release, len(tray) - 1))
+ # the rig shrinks and rises in beat 8 to make room for the graph
m = suave(T.p(f, 8))
pz = PZ_A + (PZ_B - PZ_A) * m
- k_esc = ESC * (1.0 + (K_B - 1.0) * m)
+ k_scale = SCALE * (1.0 + (K_B - 1.0) * m)
obj["hub"].location = (0, 0.06, pz)
- obj["aro"].location = (0, 0.02, pz)
- obj["hub"].scale = obj["aro"].scale = (1 - 0.3 * m,) * 3
+ obj["hoop"].location = (0, 0.02, pz)
+ obj["hub"].scale = obj["hoop"].scale = (1 - 0.3 * m,) * 3
- abanico = (1.0 - suave(T.p(f, 0))) * 0.34 if f < f_suelta else 0.0
+ fan = (1.0 - suave(T.p(f, 0))) * 0.34 if f < f_release else 0.0
for k, g in enumerate(pend):
(cx, cz), (px, pz2) = tray[idx][k]
- if abanico:
- ang = TH0 + (k - 1) * abanico
+ if fan:
+ ang = TH0 + (k - 1) * fan
cx, cz = L1 * math.sin(ang), -L1 * math.cos(ang)
px, pz2 = cx + L2 * math.sin(ang), cz - L2 * math.cos(ang)
y = g["y"]
o = (0.0, y, pz)
- codo = (cx * k_esc, y, pz + cz * k_esc)
- punta = (px * k_esc, y, pz + pz2 * k_esc)
- orientar(g["v1"], o, codo)
- orientar(g["v2"], codo, punta)
- g["v1"].scale = (1, 1, L1 * k_esc)
- g["v2"].scale = (1, 1, L2 * k_esc)
- g["codo"].location = codo
- g["punta"].location = punta
+ elbow = (cx * k_scale, y, pz + cz * k_scale)
+ tip_pt = (px * k_scale, y, pz + pz2 * k_scale)
+ orient(g["v1"], o, elbow)
+ orient(g["v2"], elbow, tip_pt)
+ g["v1"].scale = (1, 1, L1 * k_scale)
+ g["v2"].scale = (1, 1, L2 * k_scale)
+ g["codo"].location = elbow
+ g["punta"].location = tip_pt
g["codo"].scale = g["punta"].scale = (1 - 0.3 * m,) * 3
- # la estela sale de la trayectoria, no se acumula: vale con frames salteados
+ # the trail comes from the trajectory, it does not accumulate: works with skipped frames
pts, rad = [], []
for j in range(ESTELA):
i2 = idx - (ESTELA - 1 - j)
q = tray[max(0, i2)][k][1]
- pts.append((q[0] * k_esc, y, pz + q[1] * k_esc))
- vivo = 1.0 if (i2 > 0 and f >= f_suelta) else 0.0
- rad.append(vivo * (j / (ESTELA - 1.0)) ** 2.2)
- rehacer_curva(g["estela"], [pts], [rad])
+ pts.append((q[0] * k_scale, y, pz + q[1] * k_scale))
+ alive = 1.0 if (i2 > 0 and f >= f_release) else 0.0
+ rad.append(alive * (j / (ESTELA - 1.0)) ** 2.2)
+ rebuild_curve(g["estela"], [pts], [rad])
- vis = suave(T.p(f, 1)) * (1.0 - suave(T.p(f, 3)))
- for t, p in obj["leyenda"]:
- t.scale = p.scale = (vis, vis, vis)
+ visible = suave(T.p(f, 1)) * (1.0 - suave(T.p(f, 3)))
+ for t, p in obj["legend"]:
+ t.scale = p.scale = (visible, visible, visible)
gv = suave(T.p(f, 8))
for k in ("x", "y", "arr", "aba"):
- graf[k].scale = (gv, gv, gv)
- n_g = min(len(sep), int(8.0 * FPS)) # ventana: los primeros 8 s
- avance = suave((T.p(f, 8) - 0.12) / 0.6) * n_g
+ graph[k].scale = (gv, gv, gv)
+ n_g = min(len(sep), int(8.0 * FPS)) # window: the first 8 s
+ advance = suave((T.p(f, 8) - 0.12) / 0.6) * n_g
pts, rad = [], []
for i in range(n_g):
x = GX0 + GW * i / (n_g - 1.0)
v = (math.log10(max(sep[i], 1e-3)) - LMIN) / (LMAX - LMIN)
pts.append((x, 0.25, GZ0 + GH * max(0.0, min(1.0, v))))
- rad.append(1.0 if i <= avance else 0.0)
- if len(graf["curva"].data.splines[0].points) != len(pts):
- graf["curva"].data.splines.clear()
- sp = graf["curva"].data.splines.new('POLY')
+ rad.append(1.0 if i <= advance else 0.0)
+ if len(graph["curva"].data.splines[0].points) != len(pts):
+ graph["curva"].data.splines.clear()
+ sp = graph["curva"].data.splines.new('POLY')
sp.points.add(len(pts) - 1)
- rehacer_curva(graf["curva"], [pts], [[r * gv for r in rad]])
+ rebuild_curve(graph["curva"], [pts], [[r * gv for r in rad]])
- if os.environ.get("MODO") == "sim":
+ if env("MODE", "MODO") == "sim":
for s in (0, 1, 2, 2.5, 3, 4, 5, 6, 8):
i = min(int(s * FPS), len(sep) - 1)
print(f" t={s:4.1f}s separacion = {sep[i]*100:8.2f} cm")
return
- render_secuencia(NOMBRE, T, actualizar)
+ render_sequence(NAME_KEY, T, refresh)
main()