aboutsummaryrefslogtreecommitdiffstats
path: root/blender/domino.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/domino.py
parent59355909f2de9236af8168a26c70bcf6caa3b285 (diff)
download100cia-videos-main.tar.gz
100cia-videos-main.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/domino.py')
-rw-r--r--blender/domino.py462
1 files changed, 231 insertions, 231 deletions
diff --git a/blender/domino.py b/blender/domino.py
index bf5f681..dac26a7 100644
--- a/blender/domino.py
+++ b/blender/domino.py
@@ -1,82 +1,82 @@
# -*- coding: utf-8 -*-
-"""DOMINO - una ficha de 5 mm, cada una 1,5 veces mas grande que la anterior.
+"""DOMINO - a 5 mm tile, each one 1.5 times bigger than the previous.
-Parte real: 13 fichas (5 mm a 65 cm) simuladas en Godot (Jolt), a escala real.
-Parte imaginada: las fichas 14 a 36 en pie, comparadas con una persona, el
-Obelisco, el Empire State y el Aconcagua. Las alturas son exactas:
- h(n) = 5 mm * 1,5^(n-1)
+Real part: 13 tiles (5 mm to 65 cm) simulated in Godot (Jolt), at real scale.
+Imagined part: tiles 14 to 36 standing, compared with a person, the Obelisco,
+the Empire State and the Aconcagua. The heights are exact:
+ h(n) = 5 mm * 1.5^(n-1)
- MODO=sim blender -b -P blender/domino.py # exporta y corre Godot
- PRUEBA=1 (con MODO=sim) simula sin timeline, para ajustar la separacion
- blender -b -P blender/domino.py # renderiza
+ MODE=sim blender -b -P blender/domino.py # exports and runs Godot
+ TEST=1 (with MODE=sim) simulates without a timeline, to tune the spacing
+ blender -b -P blender/domino.py # renders
"""
import math, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bpy
from base import *
-NOMBRE = "domino"
-H0 = 0.005 # la primera ficha
-R = 1.5 # cada una, 1,5 veces la anterior
+NAME_KEY = "domino"
+H0 = 0.005 # the first tile
+R = 1.5 # each one 1.5 times the previous
N_REAL = 13
N_TOTAL = 36
-ANCHO, GROSOR = 0.50, float(os.environ.get("T_GR", 0.10)) # proporciones de la ficha respecto del alto
-SEP = float(os.environ.get("T_SEP", 0.70)) # hueco entre fichas, en altos de la anterior
-FRIC = float(os.environ.get("T_FR", 0.7))
-FPS_SIM = 240 # se graba a 240 fps: permite la camara lenta x8
+WIDTH, THICKNESS = 0.50, float(env("T_THICK", None, 0.10)) # tile proportions relative to its height
+SEP = float(os.environ.get("T_SEP", 0.70)) # gap between tiles, in heights of the previous one
+FRICTION = float(env("T_FRIC", None, 0.7))
+FPS_SIM = 240 # recorded at 240 fps: allows x8 slow motion
def alto(n):
return H0 * R ** (n - 1)
-def posiciones(n_max=N_TOTAL):
- """Centro en x de cada ficha (1-based: xs[1] es la primera)."""
+def positions(n_max=N_TOTAL):
+ """Centre x of each tile (1-based: xs[1] is the first)."""
xs = [0.0, 0.0]
for n in range(1, n_max):
h, h2 = alto(n), alto(n + 1)
- xs.append(xs[-1] + GROSOR * h / 2 + SEP * h + GROSOR * h2 / 2)
+ xs.append(xs[-1] + THICKNESS * h / 2 + SEP * h + THICKNESS * h2 / 2)
return xs
-def construir():
- escena(muestras=int(os.environ.get("MUESTRAS", 24)))
- mundo_estudio(fuerza=0.6)
- xs = posiciones()
+def build_scene():
+ scene_setup(samples=int(env("SAMPLES", "MUESTRAS", 24)))
+ studio_world(force=0.6)
+ xs = positions()
M = {
- "ficha": material("ficha", "#15171C", rug=0.18),
- "punto": material("punto", "#F2F2F2", rug=0.3),
- "piso": madera("piso", claro="#8C6239", oscuro="#4A2E18", escala=3.0, rug=0.75,
- veta=(0.5, 1, 1)),
+ "ficha": material("ficha", "#15171C", rough=0.18),
+ "punto": material("punto", "#F2F2F2", rough=0.3),
+ "piso": madera("piso", light_c="#8C6239", dark="#4A2E18", scale_to=3.0, rough=0.75,
+ grain=(0.5, 1, 1)),
"inv": material("inv", "gris"),
}
- fichas = {}
+ tile_list = {}
for n in range(1, N_TOTAL + 1):
h = alto(n)
- nombre = f"bloque_d{n:02d}" if n <= N_REAL else f"ficha_{n:02d}"
- ob = caja(nombre, (GROSOR * h, ANCHO * h, h), (xs[n], 0, h / 2), M["ficha"])
- # los puntos del domino, en la cara que mira a la camara (-X) y la opuesta
- for lado in (-1, 1):
+ obj_name = f"block_d{n:02d}" if n <= N_REAL else f"ficha_{n:02d}"
+ ob = box_obj(obj_name, (THICKNESS * h, WIDTH * h, h), (xs[n], 0, h / 2), M["ficha"])
+ # the domino pips, on the face looking at the camera (-X) and the opposite one
+ for side in (-1, 1):
for (py, pz) in pips(n):
- p = esfera(f"pip_{n}_{lado}_{py}_{pz}", 0.055 * h, M["punto"], seg=12, anillos=6)
+ p = sphere(f"pip_{n}_{side}_{py}_{pz}", 0.055 * h, M["punto"], seg_m=12, rings=6)
p.scale = (0.35, 1, 1)
- p.location = (lado * GROSOR * h / 2, py * h, pz * h)
+ p.location = (side * THICKNESS * h / 2, py * h, pz * h)
p.parent = ob
- raya = caja(f"raya_{n}", (GROSOR * h * 1.02, ANCHO * h * 0.8, 0.012 * h), (0, 0, 0),
+ stripe = box_obj(f"raya_{n}", (THICKNESS * h * 1.02, WIDTH * h * 0.8, 0.012 * h), (0, 0, 0),
M["punto"])
- raya.parent = ob
- fichas[n] = ob
- inclinar(fichas[1], xs[1], INCL)
- piso = caja("caja_piso", (4.0, 3.0, 0.2), (1.0, 0, -0.1), M["inv"])
- piso.hide_render = True
- return dict(fichas=fichas, xs=xs, M=M, piso=piso)
+ stripe.parent = ob
+ tile_list[n] = ob
+ tilt(tile_list[1], xs[1], INCL)
+ floor_obj = box_obj("box_piso", (4.0, 3.0, 0.2), (1.0, 0, -0.1), M["inv"])
+ floor_obj.hide_render = True
+ return dict(tile_list=tile_list, xs=xs, M=M, floor_obj=floor_obj)
-INCL = math.radians(12.0) # la primera arranca pasada su inclinacion de vuelco (9 grados)
+INCL = math.radians(12.0) # the first one starts past its tipping angle (9 degrees)
-def inclinar(ob, x, ang):
- """Gira la ficha sobre su canto delantero de abajo (el que mira a +X)."""
+def tilt(ob, x, ang):
+ """Rotates the tile about its lower front edge (the one facing +X)."""
h = ob.dimensions.z
t = ob.dimensions.x
c, s_ = math.cos(ang), math.sin(ang)
@@ -85,7 +85,7 @@ def inclinar(ob, x, ang):
def pips(n):
- """Puntos (y, z relativos al alto) de un domino: dos mitades con 0..6."""
+ """Pips (y, z relative to the height) of a domino: two halves with 0..6."""
pat = {0: [], 1: [(0, 0)], 2: [(-1, 1), (1, -1)], 3: [(-1, 1), (0, 0), (1, -1)],
4: [(-1, 1), (1, 1), (-1, -1), (1, -1)],
5: [(-1, 1), (1, 1), (0, 0), (-1, -1), (1, -1)],
@@ -98,51 +98,51 @@ def pips(n):
return out
-def cuadro_suelta(T):
- """El lapiz la empuja al final del beat 3 ("le doy un toquecito...")."""
- a, z = T.rango(3)
+def frame_release(T):
+ """The pencil pushes it at the end of beat 3 ("le doy un toquecito...")."""
+ a, z = T.span(3)
return a + int(0.78 * (z - a))
-def simular(t_suelta, duracion, obj):
- reales = [obj["fichas"][n] for n in range(1, N_REAL + 1)]
+def simulate(t_release, duration, obj):
+ real_ones = [obj["tile_list"][n] for n in range(1, N_REAL + 1)]
cfg = {
- "duracion": duracion,
- "gravedad": 9.81,
- "escala": 40.0,
+ "duration": duration,
+ "gravity": 9.81,
+ "scale": 40.0,
"hz": 7680,
"fps": FPS_SIM,
- "congelados": ["bloque_"],
- "reglas": {"bloque_": {"friccion": FRIC, "rebote": 0.0, "densidad": 1100},
- "caja_": {"friccion": FRIC, "rebote": 0.0}},
- "eventos": [{"t": t_suelta, "accion": "soltar", "prefijo": "bloque_"}],
+ "frozen": ["block_"],
+ "rules": {"block_": {"friction": FRICTION, "bounce": 0.0, "density": 1100},
+ "box_": {"friction": FRICTION, "bounce": 0.0}},
+ "events": [{"t": t_release, "action": "release", "prefix": "block_"}],
}
- correr_godot(exportar_fisica(NOMBRE, reales + [obj["piso"]], cfg))
+ run_godot(export_physics(NAME_KEY, real_ones + [obj["floor_obj"]], cfg))
-# --- escenografia visual (no participa de la fisica) ---------------------------
-def gris(nombre, col, rug=0.6):
- return material(nombre, col, rug=rug)
+# --- visual scenery (takes no part in the physics) -----------------------------
+def gris(obj_name, col, rough=0.6):
+ return material(obj_name, col, rough=rough)
def persona(x, y):
m = gris("persona", "#C9D1DC")
g = bpy.data.objects.new("persona", None)
bpy.context.collection.objects.link(g)
- partes = []
+ parts = []
for sx in (-0.1, 0.1):
- c = cilindro(f"pierna{sx}", 0.075, 0.85, m); c.location = (0, sx, 0.425); partes.append(c)
- t = cilindro("torso", 0.19, 0.62, m); t.location = (0, 0, 1.17); partes.append(t)
+ c = cylinder(f"pierna{sx}", 0.075, 0.85, m); c.location = (0, sx, 0.425); parts.append(c)
+ t = cylinder("torso", 0.19, 0.62, m); t.location = (0, 0, 1.17); parts.append(t)
for sy in (-0.26, 0.26):
- b = cilindro(f"brazo{sy}", 0.055, 0.62, m); b.location = (0, sy, 1.15); partes.append(b)
- cab = esfera("cabeza", 0.12, m); cab.location = (0, 0, 1.62); partes.append(cab)
- for p_ in partes:
+ b = cylinder(f"brazo{sy}", 0.055, 0.62, m); b.location = (0, sy, 1.15); parts.append(b)
+ header = sphere("cabeza", 0.12, m); header.location = (0, 0, 1.62); parts.append(header)
+ for p_ in parts:
p_.parent = g
g.location = (x, y, 0)
return g
-def obelisco(x, y):
+def obelisk(x, y):
m = gris("obelisco", "#EDEFF2", 0.5)
b0, b1, h = 6.8 / 2, 3.5 / 2, 63.5
v = [(sx * b0, sy * b0, 0) for sx, sy in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
@@ -150,7 +150,7 @@ def obelisco(x, y):
v += [(0, 0, 67.5)]
f = [(0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7), (4, 5, 8), (5, 6, 8),
(6, 7, 8), (7, 4, 8), (3, 2, 1, 0)]
- ob = objeto("obelisco", malla_de("obelisco", v, f, suave=False), m)
+ ob = make_object("obelisco", mesh_from("obelisco", v, f, suave=False), m)
ob.location = (x, y, 0)
return ob
@@ -161,27 +161,27 @@ def empire(x, y):
bpy.context.collection.objects.link(g)
for (w, d, z0, z1) in ((130, 58, 0, 25), (95, 50, 25, 250), (70, 42, 250, 290),
(48, 34, 290, 330), (26, 26, 330, 381)):
- c = caja(f"es{z0}", (w, d, z1 - z0), (0, 0, (z0 + z1) / 2), m)
+ c = box_obj(f"es{z0}", (w, d, z1 - z0), (0, 0, (z0 + z1) / 2), m)
c.parent = g
- a = cilindro("antena", 2.5, 62, m); a.location = (0, 0, 381 + 31); a.parent = g
+ a = cylinder("antena", 2.5, 62, m); a.location = (0, 0, 381 + 31); a.parent = g
g.location = (x, y, 0)
return g
def montana(x, y, alto_m=6961.0, radio=9000.0):
- """Cono con ruido; nieve arriba por un material que depende de la altura."""
+ """Cone with noise; snow on top through a height-dependent material."""
import random
rnd = random.Random(5)
n_r, n_a = 40, 96
v, f = [(0, 0, alto_m)], []
- fase = [rnd.uniform(0, 6.28) for _ in range(4)]
+ phase = [rnd.uniform(0, 6.28) for _ in range(4)]
for i in range(1, n_r + 1):
r = radio * i / n_r
for j in range(n_a):
a = 2 * math.pi * j / n_a
- ruido = sum(0.10 / (k + 1) * math.sin((k + 2) * a + fase[k] + 0.0007 * r * (k + 1))
+ noise = sum(0.10 / (k + 1) * math.sin((k + 2) * a + phase[k] + 0.0007 * r * (k + 1))
for k in range(4))
- z = alto_m * max(0.0, 1 - r / radio) ** 1.25 * (1 + ruido * (i / n_r))
+ z = alto_m * max(0.0, 1 - r / radio) ** 1.25 * (1 + noise * (i / n_r))
v.append((r * math.cos(a), r * math.sin(a), max(0.0, z)))
for j in range(n_a):
f.append((0, 1 + j, 1 + (j + 1) % n_a))
@@ -199,172 +199,172 @@ def montana(x, y, alto_m=6961.0, radio=9000.0):
mr = nt.nodes.new("ShaderNodeMapRange")
mr.inputs[1].default_value = alto_m * 0.62
mr.inputs[2].default_value = alto_m * 0.72
- rampa = nt.nodes.new("ShaderNodeValToRGB")
- rampa.color_ramp.elements[0].color = (*srgb("#5A5048"), 1)
- rampa.color_ramp.elements[1].color = (*srgb("#F4F6FA"), 1)
+ ramp = nt.nodes.new("ShaderNodeValToRGB")
+ ramp.color_ramp.elements[0].color = (*srgb("#5A5048"), 1)
+ ramp.color_ramp.elements[1].color = (*srgb("#F4F6FA"), 1)
nt.links.new(tc.outputs["Object"], sep.inputs[0])
nt.links.new(sep.outputs["Z"], mr.inputs[0])
- nt.links.new(mr.outputs[0], rampa.inputs[0])
- nt.links.new(rampa.outputs["Color"], b.inputs["Base Color"])
- poner(b, "Roughness", 0.8)
- ob = objeto("montana", malla_de("montana", v, f), m)
+ nt.links.new(mr.outputs[0], ramp.inputs[0])
+ nt.links.new(ramp.outputs["Color"], b.inputs["Base Color"])
+ put(b, "Roughness", 0.8)
+ ob = make_object("montana", mesh_from("montana", v, f), m)
ob.location = (x, y, 0)
return ob
-def casa(x, y):
+def home(x, y):
m = gris("casa", "#D8C7A8", 0.7)
mt = gris("techo", "#8C3B2E", 0.6)
g = bpy.data.objects.new("casa", None)
bpy.context.collection.objects.link(g)
- c = caja("casa_c", (9, 7, 5), (0, 0, 2.5), m); c.parent = g
+ c = box_obj("casa_c", (9, 7, 5), (0, 0, 2.5), m); c.parent = g
v = [(-4.8, -3.8, 5), (4.8, -3.8, 5), (4.8, 3.8, 5), (-4.8, 3.8, 5), (-4.8, 0, 8), (4.8, 0, 8)]
f = [(0, 1, 5, 4), (2, 3, 4, 5), (0, 4, 3), (1, 2, 5), (3, 2, 1, 0)]
- t = objeto("techo", malla_de("techo", v, f, suave=False), mt); t.parent = g
+ t = make_object("techo", mesh_from("techo", v, f, suave=False), mt); t.parent = g
g.location = (x, y, 0)
return g
-def moneda(x, y):
- m = metal("moneda", "#D8B45A", rug=0.25)
- c = cilindro("moneda", 0.0115, 0.0022, m, lados=48)
+def coin(x, y):
+ m = metal("moneda", "#D8B45A", rough=0.25)
+ c = cylinder("moneda", 0.0115, 0.0022, m, sides=48)
c.location = (x, y, 0.0011)
return c
-def lapiz():
+def pencil():
g = bpy.data.objects.new("lapiz", None)
bpy.context.collection.objects.link(g)
- cu = cilindro("lapiz_cuerpo", 0.0035, 0.12, material("amarillo", "#F2B61B", rug=0.4), lados=6)
+ cu = cylinder("lapiz_cuerpo", 0.0035, 0.12, material("amarillo", "#F2B61B", rough=0.4), sides=6)
cu.location = (0, 0, 0.06 + 0.012)
cu.parent = g
- # punta: cono de madera con grafito
+ # tip: wooden cone with graphite
v = [(0.0035 * math.cos(2 * math.pi * k / 12), 0.0035 * math.sin(2 * math.pi * k / 12), 0.012)
for k in range(12)] + [(0, 0, 0)]
f = [(k, (k + 1) % 12, 12) for k in range(12)]
- pt = objeto("lapiz_punta", malla_de("lapiz_punta", v, f), material("madera_l", "#E8C79A"))
+ pt = make_object("lapiz_punta", mesh_from("lapiz_punta", v, f), material("madera_l", "#E8C79A"))
pt.parent = g
return g
def main():
- obj = construir()
- if os.environ.get("MODO") == "sim" and os.environ.get("PRUEBA"):
- simular(0.2, 7.0, obj)
+ obj = build_scene()
+ if env("MODE", "MODO") == "sim" and env("TEST", "PRUEBA"):
+ simulate(0.2, 7.0, obj)
return
- T = Tiempo(NOMBRE)
- if os.environ.get("MODO") == "sim":
- simular((cuadro_suelta(T) - 1) / FPS, T.n_frames / FPS + 0.5, obj)
+ T = Timeline(NAME_KEY)
+ if env("MODE", "MODO") == "sim":
+ simulate((frame_release(T) - 1) / FPS, T.n_frames / FPS + 0.5, obj)
return
render(T, obj)
def render(T, obj):
import numpy as np
- idx, D = cargar_sim(NOMBRE)
- fichas, xs = obj["fichas"], obj["xs"]
- r = T.rango
- fr = cuadro_suelta(T)
- t_suelta = (fr - 1) / FPS
- i0 = int(round(t_suelta * FPS_SIM))
-
- # cuando cae cada ficha (inclinacion > 30 grados), en segundos desde la suelta
- caida = {}
+ idx, D = load_sim(NAME_KEY)
+ tile_list, xs = obj["tile_list"], obj["xs"]
+ r = T.span
+ fr = frame_release(T)
+ t_release = (fr - 1) / FPS
+ i0 = int(round(t_release * FPS_SIM))
+
+ # when each tile falls (tilt > 30 degrees), in seconds since the release
+ fall = {}
for n in range(1, N_REAL + 1):
- qw = np.abs(D[:, idx[f"bloque_d{n:02d}"], 3])
+ qw = np.abs(D[:, idx[f"block_d{n:02d}"], 3])
ang = 2 * np.degrees(np.arccos(np.clip(qw, 0, 1)))
k = int(np.argmax(ang > 30)) if (ang > 30).any() else len(D) - 1
- caida[n] = (k - i0) / FPS_SIM
- t_cadena = caida[N_REAL] + 0.6
- print(f"[{NOMBRE}] caidas: " + " ".join(f"{n}:{caida[n]:.2f}" for n in caida))
+ fall[n] = (k - i0) / FPS_SIM
+ t_chain = fall[N_REAL] + 0.6
+ print(f"[{NAME_KEY}] caidas: " + " ".join(f"{n}:{fall[n]:.2f}" for n in fall))
- # las imaginarias arrancan despues de donde quedo acostada la 13
- x_img = {N_REAL + 1: xs[N_REAL] + 1.25 * alto(N_REAL) + GROSOR * alto(N_REAL + 1)}
+ # the imaginary ones start past where tile 13 ended up lying
+ x_img = {N_REAL + 1: xs[N_REAL] + 1.25 * alto(N_REAL) + THICKNESS * alto(N_REAL + 1)}
for n in range(N_REAL + 1, N_TOTAL):
h, h2 = alto(n), alto(n + 1)
- x_img[n + 1] = x_img[n] + GROSOR * h / 2 + SEP * h + GROSOR * h2 / 2
+ x_img[n + 1] = x_img[n] + THICKNESS * h / 2 + SEP * h + THICKNESS * h2 / 2
for n in range(N_REAL + 1, N_TOTAL + 1):
- fichas[n].location.x = x_img[n]
+ tile_list[n].location.x = x_img[n]
X = dict(xs=xs)
X.update({n: xs[n] for n in range(1, N_REAL + 1)})
X.update(x_img)
- # escenografia
- mesa = caja("mesa", (6.0, 3.0, 0.02), (1.2, 0.0, -0.01), obj["M"]["piso"])
- mesa.visible_shadow = False # si no, su sombra cae sobre el suelo 2 mm mas abajo
- # el suelo se escala cada cuadro segun la distancia de la camara: un plano de
- # 200 km fijo tiene triangulos tan grandes que la profundidad pierde
- # precision y tapa a la mesa (que esta 2 mm mas arriba)
- suelo = caja("suelo", (1.0, 1.0, 0.02), (0, 0, -0.012),
- material("suelo", "#2B2622", rug=0.9))
- lap = lapiz()
- # comparaciones: la camara mira en la direccion VH (del lado de la cara con
- # puntos) y cada referencia va al costado de su ficha, perpendicular a la
- # mirada: asi estan a la misma distancia de la camara y la perspectiva no
- # agranda a ninguna de las dos.
+ # scenery
+ table = box_obj("mesa", (6.0, 3.0, 0.02), (1.2, 0.0, -0.01), obj["M"]["piso"])
+ table.visible_shadow = False # otherwise its shadow falls on the ground 2 mm below
+ # the ground is scaled every frame by the camera distance: a fixed 200 km
+ # plane has triangles so large that depth loses precision and hides the
+ # table (which is 2 mm higher)
+ ground = box_obj("suelo", (1.0, 1.0, 0.02), (0, 0, -0.012),
+ material("suelo", "#2B2622", rough=0.9))
+ lap = pencil()
+ # comparisons: the camera looks along the VH direction (from the side of the
+ # face with pips) and each reference stands next to its tile, perpendicular to
+ # the view: that way both are at the same distance from the camera and
+ # perspective enlarges neither.
VH = (0.857, 0.514)
- DER = (0.514, -0.857) # "a la derecha" en pantalla
+ RIGHT_X = (0.514, -0.857) # "to the right" on screen
L = {}
for n, gap, wl, hl in ((16, 0.30, 0.25, 1.75), (25, 12.0, 3.4, 67.5),
(29, 60.0, 65.0, 443.0), (36, 600.0, 4500.0, 6961.0),
(20, 2.5, 4.8, 8.0)):
dd = 0.3 * alto(n) + gap + wl
- L[n] = (X[n] + DER[0] * dd, DER[1] * dd, dd, wl, hl)
- pers = persona(L[16][0], L[16][1])
- obe = obelisco(L[25][0], L[25][1])
- emp = empire(L[29][0], L[29][1])
+ L[n] = (X[n] + RIGHT_X[0] * dd, RIGHT_X[1] * dd, dd, wl, hl)
+ persp = persona(L[16][0], L[16][1])
+ obel = obelisk(L[25][0], L[25][1])
+ tie = empire(L[29][0], L[29][1])
mon_t = montana(L[36][0], L[36][1], radio=4500.0)
- cas = casa(L[20][0], L[20][1])
- for ob_ in (pers, emp, cas):
- ob_.rotation_euler = (0, 0, math.atan2(VH[1], VH[0])) # de frente a la camara
+ house = home(L[20][0], L[20][1])
+ for ob_ in (persp, tie, house):
+ ob_.rotation_euler = (0, 0, math.atan2(VH[1], VH[0])) # facing the camera
- def etiqueta(txt, col, tam, loc, giro=-0.5):
- t = texto(txt, tam=tam, color=col)
+ def label(txt, col, size_u, loc, giro=-0.5):
+ t = txt_m(txt, size_u=size_u, color=col)
t.location = loc
t.rotation_euler = (math.pi / 2, 0, giro)
t.visible_shadow = False
return t
E = {
- "5mm": etiqueta("5 mm", "ambar", 0.0035, (xs[1], -0.006, alto(1) + 0.004)),
- "x15": etiqueta("×1,5", "ambar", 0.005, ((xs[1] + xs[2]) / 2, -0.01, alto(2) + 0.006)),
- "65": etiqueta("65 cm", "ambar", 0.08, (xs[13], -0.1, alto(13) + 0.09)),
- "16": etiqueta("16", "ambar", 0.40, (X[16], 0, 1.07 * alto(16) + 0.40 * 0.5), -1.03),
- "vos": etiqueta("vos", "blanco", 0.26, (L[16][0], L[16][1], 2.0), -1.03),
- "25": etiqueta("25", "ambar", 11, (X[25], 0, 1.07 * alto(25) + 11 * 0.5), -1.03),
- "obe": etiqueta("Obelisco", "blanco", 7, (L[25][0], L[25][1], 75), -1.03),
- "29": etiqueta("29", "ambar", 55, (X[29], 0, 1.07 * alto(29) + 55 * 0.5), -1.03),
- "emp": etiqueta("Empire State", "blanco", 38, (L[29][0], L[29][1], 490), -1.03),
- "36": etiqueta("36", "ambar", 900, (X[36], 0, 1.07 * alto(36) + 900 * 0.5), -1.03),
- "aco": etiqueta("Aconcagua", "blanco", 700, (L[36][0], L[36][1], 7700), -1.03),
- "casa": etiqueta("casa: 8 m", "blanco", 1.1, (L[20][0], L[20][1], 9.3), -1.03),
+ "5mm": label("5 mm", "ambar", 0.0035, (xs[1], -0.006, alto(1) + 0.004)),
+ "x15": label("×1,5", "ambar", 0.005, ((xs[1] + xs[2]) / 2, -0.01, alto(2) + 0.006)),
+ "65": label("65 cm", "ambar", 0.08, (xs[13], -0.1, alto(13) + 0.09)),
+ "16": label("16", "ambar", 0.40, (X[16], 0, 1.07 * alto(16) + 0.40 * 0.5), -1.03),
+ "vos": label("vos", "blanco", 0.26, (L[16][0], L[16][1], 2.0), -1.03),
+ "25": label("25", "ambar", 11, (X[25], 0, 1.07 * alto(25) + 11 * 0.5), -1.03),
+ "obe": label("Obelisco", "blanco", 7, (L[25][0], L[25][1], 75), -1.03),
+ "29": label("29", "ambar", 55, (X[29], 0, 1.07 * alto(29) + 55 * 0.5), -1.03),
+ "emp": label("Empire State", "blanco", 38, (L[29][0], L[29][1], 490), -1.03),
+ "36": label("36", "ambar", 900, (X[36], 0, 1.07 * alto(36) + 900 * 0.5), -1.03),
+ "aco": label("Aconcagua", "blanco", 700, (L[36][0], L[36][1], 7700), -1.03),
+ "casa": label("casa: 8 m", "blanco", 1.1, (L[20][0], L[20][1], 9.3), -1.03),
}
- xs15 = [etiqueta("×1,5", "ambar", 0.35 * alto(n), (xs[n], -0.1 * alto(n), 0.22 * alto(n) + 0.02))
+ xs15 = [label("×1,5", "ambar", 0.35 * alto(n), (xs[n], -0.1 * alto(n), 0.22 * alto(n) + 0.02))
for n in range(2, N_REAL + 1)]
- lente = 50.0
- cam = camara((0, -1, 0.3), (0, 0, 0), lente=lente)
+ lens = 50.0
+ cam = camera_obj((0, -1, 0.3), (0, 0, 0), lens=lens)
cam.data.sensor_fit = 'VERTICAL'
cam.data.sensor_height = 36.0
- sol = bpy.data.lights.new("sol", 'SUN')
- sol.energy = 3.2
- sol.angle = math.radians(3)
- so = bpy.data.objects.new("sol", sol)
+ sun = bpy.data.lights.new("sol", 'SUN')
+ sun.energy = 3.2
+ sun.angle = math.radians(3)
+ so = bpy.data.objects.new("sol", sun)
bpy.context.collection.objects.link(so)
so.rotation_euler = (math.radians(50), math.radians(-18), math.radians(-35))
- def frente(t):
- """Indice continuo de la ficha que esta cayendo a los t s de soltar."""
+ def front_side(t):
+ """Continuous index of the tile falling t s after the release."""
if t <= 0:
return 1.0
for n in range(1, N_REAL):
- if t < caida[n + 1]:
- a, b = caida[n], caida[n + 1]
+ if t < fall[n + 1]:
+ a, b = fall[n], fall[n + 1]
return n + max(0.0, min(1.0, (t - a) / max(1e-6, b - a)))
return float(N_REAL)
- def cam_frente(t):
- u = frente(t)
+ def cam_front(t):
+ u = front_side(t)
n = int(u)
w = u - n
n2 = min(N_REAL, n + 1)
@@ -374,15 +374,15 @@ def render(T, obj):
VISTA13 = ((xs[1] + xs[13]) / 2 + 0.25, 0.32, 2.3)
- def toma_ref(n):
- """Encuadre de la ficha n con su referencia a la derecha: (x, z, alto, y)."""
+ def take_ref(n):
+ """Framing of tile n with its reference to the right: (x, z, height, y)."""
h = alto(n)
_, _, dd, wl, hl = L[n]
- izq = -(1.0 if n == 20 else 0.7) * h # algo de las fichas anteriores
- der = dd + wl
- c = (izq + der) / 2
- alto_v = max(max(h, hl) * 1.75, (der - izq) / 0.5625 * 1.3)
- return (X[n] + DER[0] * c, max(h, hl) * 0.50, alto_v, DER[1] * c)
+ left_part = -(1.0 if n == 20 else 0.7) * h # a bit of the previous tiles
+ right_part = dd + wl
+ c = (left_part + right_part) / 2
+ alto_v = max(max(h, hl) * 1.75, (right_part - left_part) / 0.5625 * 1.3)
+ return (X[n] + RIGHT_X[0] * c, max(h, hl) * 0.50, alto_v, RIGHT_X[1] * c)
tomas = [
(1, (xs[1] + 0.003, 0.004, 0.032)),
@@ -399,28 +399,28 @@ def render(T, obj):
(f_rep1 + 1, VISTA13),
(r(8)[1], (VISTA13[0] + 0.1, 0.3, 2.0)),
(r(9)[1], (X[15] - 1.0, 1.0, 6.0)),
- (r(10)[0] + 10, toma_ref(16)),
- (r(10)[1], toma_ref(16)),
- (r(11)[0] + 12, toma_ref(25)),
- (r(11)[1], toma_ref(25)),
- (r(12)[0] + 12, toma_ref(29)),
- (r(12)[1], toma_ref(29)),
- (r(13)[0] + 14, toma_ref(36)),
- (r(13)[1], toma_ref(36)),
+ (r(10)[0] + 10, take_ref(16)),
+ (r(10)[1], take_ref(16)),
+ (r(11)[0] + 12, take_ref(25)),
+ (r(11)[1], take_ref(25)),
+ (r(12)[0] + 12, take_ref(29)),
+ (r(12)[1], take_ref(29)),
+ (r(13)[0] + 14, take_ref(36)),
+ (r(13)[1], take_ref(36)),
(r(14)[1] - 5, (xs[1] + 0.003, 0.004, 0.032)),
(r(15)[1], VISTA13),
(r(16)[1], (VISTA13[0], 0.3, 1.9)),
- (r(17)[0] + 15, toma_ref(20)),
- (T.n_frames, toma_ref(20)),
+ (r(17)[0] + 15, take_ref(20)),
+ (T.n_frames, take_ref(20)),
]
- def interp(lista, f):
- for (f0, a), (f1, b) in zip(lista, lista[1:]):
+ def interp(item_list, f):
+ for (f0, a), (f1, b) in zip(item_list, item_list[1:]):
if f0 <= f <= f1:
u = suave((f - f0) / max(1, f1 - f0))
ha, hb = a[2], b[2]
if max(ha, hb) / min(ha, hb) > 3:
- # zoom grande: alto en escala log, el blanco se mueve con el alto
+ # big zoom: height on a log scale, the target moves with the height
h = math.exp(math.log(ha) + (math.log(hb) - math.log(ha)) * u)
w = (h - ha) / (hb - ha)
else:
@@ -429,70 +429,70 @@ def render(T, obj):
ya = a[3] if len(a) > 3 else 0.0
yb = b[3] if len(b) > 3 else 0.0
return (a[0] + (b[0] - a[0]) * w, a[1] + (b[1] - a[1]) * w, h, ya + (yb - ya) * w)
- ult = lista[-1][1] if f > lista[-1][0] else lista[0][1]
- return tuple(ult) + ((0.0,) if len(ult) == 3 else ())
+ last_pt = item_list[-1][1] if f > item_list[-1][0] else item_list[0][1]
+ return tuple(last_pt) + ((0.0,) if len(last_pt) == 3 else ())
def t_sim(f):
- """Segundos de simulacion desde la suelta que se muestran en el cuadro f."""
+ """Seconds of simulation since the release shown in frame f."""
if f < fr:
return 0.0
if f < f_rep0:
return (f - fr) / FPS
if f <= f_rep1:
u = (f - f_rep0) / max(1, f_rep1 - f_rep0)
- return t_cadena * u * u # rampa: lenta al principio
- return t_cadena + 2.0
+ return t_chain * u * u # ramp: slow at first
+ return t_chain + 2.0
- def actualizar(f):
+ def refresh(f):
ts = t_sim(f)
i = max(0, min(len(D) - 1, i0 + int(round(ts * FPS_SIM))))
for n in range(1, N_REAL + 1):
- poner_pose(fichas[n], D[i, idx[f"bloque_d{n:02d}"]])
- # antes de soltar, la primera esta derecha y el lapiz la inclina
- f_emp = fr - 22
+ set_pose(tile_list[n], D[i, idx[f"block_d{n:02d}"]])
+ # before the release, the first tile stands upright and the pencil tilts it
+ f_tie = fr - 22
if f < fr:
- u = suave((f - f_emp) / 22.0) if f >= f_emp else 0.0
- fichas[1].rotation_mode = 'XYZ'
- inclinar(fichas[1], xs[1], INCL * u)
+ u = suave((f - f_tie) / 22.0) if f >= f_tie else 0.0
+ tile_list[1].rotation_mode = 'XYZ'
+ tilt(tile_list[1], xs[1], INCL * u)
h1 = alto(1)
- lap_vis = (fr - 60) <= f <= fr + 25
- lap.hide_render = not lap_vis
+ pencil_vis = (fr - 60) <= f <= fr + 25
+ lap.hide_render = not pencil_vis
for c in lap.children:
- c.hide_render = not lap_vis
- if lap_vis:
- llega = suave((f - (fr - 60)) / 38.0)
- vuelve = suave((f - fr) / 20.0)
- u = suave((f - f_emp) / 22.0) if f >= f_emp else 0.0
- tip_x = xs[1] - GROSOR * h1 / 2 + h1 * math.sin(INCL * u) - 0.02 * (1 - llega) - 0.01 * vuelve
- lap.location = (tip_x, 0.0, 0.9 * h1 + 0.02 * (1 - llega) + 0.01 * vuelve)
+ c.hide_render = not pencil_vis
+ if pencil_vis:
+ arrives = suave((f - (fr - 60)) / 38.0)
+ returns = suave((f - fr) / 20.0)
+ u = suave((f - f_tie) / 22.0) if f >= f_tie else 0.0
+ tip_x = xs[1] - THICKNESS * h1 / 2 + h1 * math.sin(INCL * u) - 0.02 * (1 - arrives) - 0.01 * returns
+ lap.location = (tip_x, 0.0, 0.9 * h1 + 0.02 * (1 - arrives) + 0.01 * returns)
lap.rotation_euler = (0, math.radians(-62), 0)
- # imaginarias
- # hasta que ficha se ve: crece con la narracion
+ # imaginary ones
+ # up to which tile is visible: it grows with the narration
if f < r(9)[0]:
- n_vis = N_REAL
+ n_visible = N_REAL
elif f < r(10)[0]:
- n_vis = N_REAL + 3 * T.p(f, 9)
+ n_visible = N_REAL + 3 * T.p(f, 9)
elif f < r(11)[0]:
- n_vis = 16
+ n_visible = 16
elif f < r(12)[0]:
- n_vis = 16 + 9 * suave(T.p(f, 11) / 0.2)
+ n_visible = 16 + 9 * suave(T.p(f, 11) / 0.2)
elif f < r(13)[0]:
- n_vis = 25 + 4 * suave(T.p(f, 12) / 0.2)
+ n_visible = 25 + 4 * suave(T.p(f, 12) / 0.2)
elif f < r(17)[0]:
- n_vis = 29 + 7 * suave(T.p(f, 13) / 0.2)
+ n_visible = 29 + 7 * suave(T.p(f, 13) / 0.2)
else:
- n_vis = 20
+ n_visible = 20
for n in range(N_REAL + 1, N_TOTAL + 1):
- fichas[n].hide_render = n > n_vis + 1e-6
- for c in fichas[n].children:
- c.hide_render = fichas[n].hide_render
- for ob_, beat in ((pers, 10), (obe, 11), (emp, 12), (mon_t, 13), (cas, 17)):
- vis = f >= r(beat)[0] - 6 if beat != 17 else f >= r(17)[0] - 10
- ob_.hide_render = not vis
+ tile_list[n].hide_render = n > n_visible + 1e-6
+ for c in tile_list[n].children:
+ c.hide_render = tile_list[n].hide_render
+ for ob_, beat in ((persp, 10), (obel, 11), (tie, 12), (mon_t, 13), (house, 17)):
+ visible = f >= r(beat)[0] - 6 if beat != 17 else f >= r(17)[0] - 10
+ ob_.hide_render = not visible
for c in ob_.children:
- c.hide_render = not vis
- vis_lab = {
+ c.hide_render = not visible
+ visible_lab = {
"5mm": r(0)[0] <= f < r(2)[0] + 10, "x15": r(1)[0] + 15 <= f < r(2)[0] + 10,
"65": r(3)[0] <= f < fr - 30, "16": r(10)[0] <= f < r(11)[0],
"vos": r(10)[0] + 8 <= f < r(11)[0], "25": r(11)[0] <= f < r(12)[0],
@@ -501,14 +501,14 @@ def render(T, obj):
"aco": r(13)[0] + 8 <= f < r(14)[0], "casa": f >= r(17)[0] + 10,
}
for k, t in E.items():
- t.hide_render = not vis_lab[k]
+ t.hide_render = not visible_lab[k]
for j, t in enumerate(xs15):
t.hide_render = not (r(7)[0] + j * 4 <= f < r(9)[0])
- # camara
+ # camera
y = 0.0
if fr - 6 <= f < f_rep0 or f_rep0 <= f <= f_rep1:
- x, z, h = cam_frente(ts)
+ x, z, h = cam_front(ts)
if f < fr + 4:
a = interp(tomas, f)
w = suave((f - (fr - 6)) / 10.0)
@@ -517,23 +517,23 @@ def render(T, obj):
x, z, h, y = interp(tomas, f)
else:
x, z, h, y = interp(tomas2, f)
- dist = h * lente / 36.0
- # angulo: alto y cruzado para las fichas chicas, mas de frente para las comparaciones
+ dist = h * lens / 36.0
+ # angle: high and crossed for the small tiles, more frontal for the comparisons
k = suave((f - r(9)[0]) / 20.0) * (1 - suave(T.p(f, 14))) + suave((f - r(17)[0]) / 15.0)
k = max(0.0, min(1.0, k))
da, db = (-0.447, -0.894, 0.32), (-VH[0], -VH[1], 0.10)
d = tuple(da[j] + (db[j] - da[j]) * k for j in range(3))
nd = math.sqrt(sum(c * c for c in d))
- zc = z - 0.08 * h # deja lugar a los subtitulos
+ zc = z - 0.08 * h # leaves room for the subtitles
pos = (x + d[0] / nd * dist, y + d[1] / nd * dist, zc + d[2] / nd * dist)
- apuntar(cam, pos, (x, y, zc))
- lado = max(12.0, dist * 60.0)
- suelo.scale = (lado, lado, 1.0)
- suelo.location = (x, y, -0.012)
+ aim_at(cam, pos, (x, y, zc))
+ side = max(12.0, dist * 60.0)
+ ground.scale = (side, side, 1.0)
+ ground.location = (x, y, -0.012)
cam.data.clip_start = max(1e-5, dist * 0.002)
cam.data.clip_end = dist * 400
- render_secuencia(NOMBRE, T, actualizar)
+ render_sequence(NAME_KEY, T, refresh)
main()