From fafaebb051907a848a9406f9da19669c81a83a3b Mon Sep 17 00:00:00 2001 From: Elvis Claros Castro Date: Sat, 26 Sep 2026 20:50:41 -0300 Subject: Translate code, comments and logs to English; English README; configurable paths and env vars 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. --- blender/bolapeluda.py | 274 +++++++++++++++++++++++++------------------------- 1 file changed, 137 insertions(+), 137 deletions(-) (limited to 'blender/bolapeluda.py') diff --git a/blender/bolapeluda.py b/blender/bolapeluda.py index a260784..fe7a377 100644 --- a/blender/bolapeluda.py +++ b/blender/bolapeluda.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -"""BOLA PELUDA - el teorema de la bola peluda (Brouwer, 1912). +"""BOLA PELUDA - the hairy ball theorem (Brouwer, 1912). -Cada pelo se acuesta en la direccion del campo tangente v(p) = A x p, y lo hace -tanto como el campo sea fuerte: donde |v| se anula (los dos polos del eje A) el -pelo no tiene hacia donde acostarse y queda parado. O sea que el remolino no -esta dibujado a mano, sale de la misma cuenta que peina el resto. +Each hair lies down along the tangent field v(p) = A x p, as much as the +field is strong: where |v| vanishes (the two poles of axis A) the hair has +nowhere to lie and stands up. So the swirl is not drawn by hand, it comes +from the same computation that combs the rest. """ import math, os, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -12,20 +12,20 @@ import bpy from mathutils import Vector, Matrix from base import * -NOMBRE = "bolapeluda" -R = 1.15 # radio de la esfera -LARGO = 0.34 # largo del pelo -K = 7 # puntos por pelo -N = 1500 # cantidad de pelos -UMBRAL = 0.42 # |v| por debajo de esto, el pelo no llega a acostarse -CZ = 0.50 # altura del centro en el cuadro - -# Peinados: (eje, tipo). Los ejes viven casi en el plano de la pantalla (XZ) -# para que los DOS ceros caigan sobre la silueta y se vean juntos. -# rot -> campo v = A x p (peina en circulos, como las latitudes) -# mer -> campo v = A - (A.p) p (peina de un polo al otro, por meridianos) -# Los dos se anulan exactamente donde p es paralelo a A: ahi esta el remolino. -PEINADOS = [((0.80, -0.05, 0.60), "rot"), +NAME_KEY = "bolapeluda" +R = 1.15 # sphere radius +LARGO = 0.34 # hair length +K = 7 # points per hair +N = 1500 # number of hairs +THRESHOLD = 0.42 # |v| below this, the hair never lies down +CZ = 0.50 # height of the centre in the frame + +# Combings: (axis, kind). The axes lie almost in the screen plane (XZ) so the +# TWO zeros fall on the silhouette and show together. +# rot -> field v = A x p (combs in circles, like latitudes) +# mer -> field v = A - (A.p) p (combs from pole to pole, along meridians) +# Both vanish exactly where p is parallel to A: that is where the swirl is. +COMBINGS = [((0.80, -0.05, 0.60), "rot"), ((-0.58, -0.05, 0.82), "mer"), ((0.97, -0.08, -0.22), "rot"), ((0.12, -0.05, 0.99), "mer"), @@ -33,17 +33,17 @@ PEINADOS = [((0.80, -0.05, 0.60), "rot"), ((0.80, -0.05, 0.60), "rot")] -def normaliza(v): +def normalize_text(v): n = math.sqrt(sum(c * c for c in v)) return tuple(c / n for c in v) if n > 1e-9 else (0.0, 0.0, 1.0) -def cruz(a, b): +def cross_m(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) -def puntos_esfera(n): - """Espiral de Fibonacci: reparte n puntos casi parejos sobre la esfera.""" +def sphere_points(n): + """Fibonacci spiral: spreads n points almost evenly over the sphere.""" pts, ga = [], math.pi * (3 - math.sqrt(5)) for i in range(n): z = 1 - 2 * (i + 0.5) / n @@ -53,36 +53,36 @@ def puntos_esfera(n): return pts -def campo(p, eje, tipo): - if tipo == "mer": - d = sum(a * b for a, b in zip(eje, p)) - return tuple(eje[i] - d * p[i] for i in range(3)) - return cruz(eje, p) +def field(p, axis_obj, kind_m): + if kind_m == "mer": + d = sum(a * b for a, b in zip(axis_obj, p)) + return tuple(axis_obj[i] - d * p[i] for i in range(3)) + return cross_m(axis_obj, p) -def pelo(p, eje, peinado, tipo="rot", radio=R, largo=LARGO): - """Devuelve los K puntos del pelo nacido en p y cuanto vale |v| ahi.""" - v = campo(p, eje, tipo) +def hair(p, axis_obj, combed, kind_m="rot", radio=R, largo=LARGO): + """Returns the K points of the hair rooted at p and the value of |v| there.""" + v = field(p, axis_obj, kind_m) m = math.sqrt(sum(c * c for c in v)) if m > 1e-6: t = tuple(c / m for c in v) - else: # en el cero no hay direccion: queda parado - t = normaliza(cruz(p, (0.0, 0.0, 1.0)) if abs(p[2]) < 0.9 else (1.0, 0.0, 0.0)) - b = peinado * min(1.0, m / UMBRAL) + else: # at the zero there is no direction: it stands up + t = normalize_text(cross_m(p, (0.0, 0.0, 1.0)) if abs(p[2]) < 0.9 else (1.0, 0.0, 0.0)) + b = combed * min(1.0, m / THRESHOLD) pos = [tuple(c * radio for c in p)] - paso = largo / (K - 1) + step = largo / (K - 1) for j in range(1, K): s = j / (K - 1.0) phi = b * (math.pi / 2) * s ** 0.85 cs, sn = math.cos(phi), math.sin(phi) d = (p[0] * cs + t[0] * sn, p[1] * cs + t[1] * sn, p[2] * cs + t[2] * sn) a = pos[-1] - pos.append((a[0] + d[0] * paso, a[1] + d[1] * paso, a[2] + d[2] * paso)) + pos.append((a[0] + d[0] * step, a[1] + d[1] * step, a[2] + d[2] * step)) return pos, m -def pelos_toro(nu=60, nv=24, RT=0.90, rt=0.34, largo=0.22): - """Pelos de la rosquilla, peinados por el tangente toroidal: nunca se anula.""" +def torus_hairs(nu=60, nv=24, RT=0.90, rt=0.34, largo=0.22): + """Donut hairs, combed by the toroidal tangent field: it never vanishes.""" splines, radios = [], [] for i in range(nu): u = 2 * math.pi * i / nu @@ -92,169 +92,169 @@ def pelos_toro(nu=60, nv=24, RT=0.90, rt=0.34, largo=0.22): cw, sw = math.cos(w), math.sin(w) p = ((RT + rt * cw) * cu, (RT + rt * cw) * su, rt * sw) n = (cw * cu, cw * su, sw) # normal - t = (-su, cu, 0.0) # tangente toroidal, |t| = 1 + t = (-su, cu, 0.0) # toroidal tangent, |t| = 1 pos = [p] - paso = largo / (K - 1) + step = largo / (K - 1) for k in range(1, K): s = k / (K - 1.0) phi = (math.pi / 2) * s ** 0.85 cs, sn = math.cos(phi), math.sin(phi) d = (n[0] * cs + t[0] * sn, n[1] * cs + t[1] * sn, n[2] * cs + t[2] * sn) a = pos[-1] - pos.append((a[0] + d[0] * paso, a[1] + d[1] * paso, a[2] + d[2] * paso)) + pos.append((a[0] + d[0] * step, a[1] + d[1] * step, a[2] + d[2] * step)) splines.append(pos) radios.append([1.0 - 0.72 * (k / (K - 1.0)) for k in range(K)]) return splines, radios -def construir(T): - sc = escena() - lente, ALTO = 70.0, 6.0 - camara((0.0, -lente / 36.0 * ALTO, 0.0), (0.0, 0.0, 0.0), lente=lente) +def build_scene(T): + sc = scene_setup() + lens, FRAME_H = 70.0, 6.0 + camera_obj((0.0, -lens / 36.0 * FRAME_H, 0.0), (0.0, 0.0, 0.0), lens=lens) bpy.context.scene.camera.data.sensor_fit = 'VERTICAL' bpy.context.scene.camera.data.sensor_height = 36.0 - luz("key", 'AREA', (-3.2, -4.6, 3.8), 800, "blanco", tam=5.0, mira=(0, 0, CZ)) - luz("fill", 'AREA', (3.8, -3.6, -1.2), 260, "celeste", tam=5.0, mira=(0, 0, CZ)) - luz("rim", 'AREA', (0.4, 4.2, 2.6), 620, "ambar", tam=4.0, mira=(0, 0, CZ)) + light_obj("key", 'AREA', (-3.2, -4.6, 3.8), 800, "blanco", size_u=5.0, sight=(0, 0, CZ)) + light_obj("fill", 'AREA', (3.8, -3.6, -1.2), 260, "celeste", size_u=5.0, sight=(0, 0, CZ)) + light_obj("rim", 'AREA', (0.4, 4.2, 2.6), 620, "ambar", size_u=4.0, sight=(0, 0, CZ)) rig = bpy.data.objects.new("rig", None) bpy.context.collection.objects.link(rig) rig.location = (0, 0, CZ) - m_piel = material("piel", "azul", rug=0.55, metal=0.1) - bola = esfera("bola", R * 0.985, m_piel, seg=64, anillos=36) - bola.parent = rig - - base_pts = puntos_esfera(N) - splines = [pelo(p, PEINADOS[0][0], 0.0)[0] for p in base_pts] - radios = [[1.0 - 0.70 * (j / (K - 1.0)) for j in range(K)] for _ in base_pts] - pel_a = curva_poly("pelo_a", splines, grosor=0.0135, radios=radios, - mat=material("m_pelo", "ambar", rug=0.42, emis=0.55)) - pel_b = curva_poly("pelo_b", splines, grosor=0.0135, radios=radios, - mat=material("m_remol", "rosa", rug=0.42, emis=1.9)) - pel_a.parent = pel_b.parent = rig - - # marcas de los ceros - marcas = [] + m_skin = material("piel", "azul", rough=0.55, metal=0.1) + ball = sphere("bola", R * 0.985, m_skin, seg_m=64, rings=36) + ball.parent = rig + + base_points = sphere_points(N) + splines = [hair(p, COMBINGS[0][0], 0.0)[0] for p in base_points] + radios = [[1.0 - 0.70 * (j / (K - 1.0)) for j in range(K)] for _ in base_points] + hair_a = curve_poly("pelo_a", splines, thickness_px=0.0135, radios=radios, + mat=material("m_pelo", "ambar", rough=0.42, emit=0.55)) + hair_b = curve_poly("pelo_b", splines, thickness_px=0.0135, radios=radios, + mat=material("m_remol", "rosa", rough=0.42, emit=1.9)) + hair_a.parent = hair_b.parent = rig + + # marks on the zeros + tick_list = [] for k in range(2): g = bpy.data.objects.new(f"marca{k}", None) bpy.context.collection.objects.link(g) g.parent = rig - anillo = toro(f"anillo{k}", 0.34, 0.024, - material(f"mm{k}", "rosa", emis=3.2), u=48, v=10) - anillo.parent = g - marcas.append(g) + ring = torus(f"anillo{k}", 0.34, 0.024, + material(f"mm{k}", "rosa", emit=3.2), u=48, v=10) + ring.parent = g + tick_list.append(g) - # la rosquilla, peinada de una - sp_t, ra_t = pelos_toro() + # the donut, combed in one go + sp_t, ra_t = torus_hairs() rig_t = bpy.data.objects.new("rig_toro", None) bpy.context.collection.objects.link(rig_t) rig_t.location = (0, 0, CZ) - rosquilla = toro("rosquilla", 0.90, 0.335, material("piel_t", "azul", rug=0.55)) - rosquilla.parent = rig_t - pelo_t = curva_poly("pelo_toro", sp_t, grosor=0.0125, radios=ra_t, - mat=material("m_pelo_t", "verde", rug=0.42, emis=0.8)) - pelo_t.parent = rig_t - rig_t.rotation_mode = 'ZYX' # primero gira sobre su eje, despues se inclina + donut = torus("rosquilla", 0.90, 0.335, material("piel_t", "azul", rough=0.55)) + donut.parent = rig_t + hair_t = curve_poly("pelo_toro", sp_t, thickness_px=0.0125, radios=ra_t, + mat=material("m_pelo_t", "verde", rough=0.42, emit=0.8)) + hair_t.parent = rig_t + rig_t.rotation_mode = 'ZYX' # first it spins about its axis, then it tilts rig_t.scale = (0, 0, 0) - etq = {"esf": texto("2", tam=0.52, color="ambar"), - "ros": texto("0", tam=0.52, color="verde"), - "pie": texto("agujeros que cuentan", tam=0.19, color="gris")} - for o in etq.values(): + tag_m = {"esf": txt_m("2", size_u=0.52, color="ambar"), + "ros": txt_m("0", size_u=0.52, color="verde"), + "pie": txt_m("agujeros que cuentan", size_u=0.19, color="gris")} + for o in tag_m.values(): o.scale = (0, 0, 0) - return dict(rig=rig, rig_t=rig_t, bola=bola, pel_a=pel_a, pel_b=pel_b, - marcas=marcas, base=base_pts, etq=etq, piel=m_piel, rosq=rosquilla) + return dict(rig=rig, rig_t=rig_t, ball=ball, hair_a=hair_a, hair_b=hair_b, + tick_list=tick_list, base=base_points, tag_m=tag_m, skin=m_skin, donut_obj=donut) def main(): - T = Tiempo(NOMBRE) - ob = construir(T) - base, pel_a, pel_b = ob["base"], ob["pel_a"], ob["pel_b"] + T = Timeline(NAME_KEY) + ob = build_scene(T) + base, hair_a, hair_b = ob["base"], ob["hair_a"], ob["hair_b"] cache = {"clave": None} - def peinar(idx_eje, peinado): - clave = (idx_eje, round(peinado, 3)) - if cache["clave"] == clave: + def comb(axis_idx, combed): + key_name = (axis_idx, round(combed, 3)) + if cache["clave"] == key_name: return - cache["clave"] = clave - eje = normaliza(PEINADOS[idx_eje][0]) - tipo = PEINADOS[idx_eje][1] + cache["clave"] = key_name + axis_obj = normalize_text(COMBINGS[axis_idx][0]) + kind_m = COMBINGS[axis_idx][1] sp, ra_a, ra_b = [], [], [] for p in base: - pos, m = pelo(p, eje, peinado, tipo) + pos, m = hair(p, axis_obj, combed, kind_m) sp.append(pos) tap = [1.0 - 0.70 * (j / (K - 1.0)) for j in range(K)] - remolino = m < UMBRAL * 0.62 and peinado > 0.25 - ra_a.append([0.0 if remolino else t for t in tap]) - ra_b.append([t * 1.25 if remolino else 0.0 for t in tap]) - rehacer_curva(pel_a, sp, ra_a) - rehacer_curva(pel_b, sp, ra_b) + swirl = m < THRESHOLD * 0.62 and combed > 0.25 + ra_a.append([0.0 if swirl else t for t in tap]) + ra_b.append([t * 1.25 if swirl else 0.0 for t in tap]) + rebuild_curve(hair_a, sp, ra_a) + rebuild_curve(hair_b, sp, ra_b) - def actualizar(f): + def refresh(f): t = T.t(f) - # se mece en vez de girar: asi los dos ceros no se van al fondo + # it rocks instead of spinning: that way the two zeros do not go to the back ob["rig"].rotation_euler = (0.0, 0.0, 0.26 * math.sin(0.42 * t)) ob["rig_t"].rotation_euler = (0.56, 0.0, 0.5 + 0.22 * t) - # que peinado toca y cuanto esta peinado - if f < T.rango(2)[0]: - idx, peinado = 0, 0.0 - elif f < T.rango(4)[0]: - idx, peinado = 0, suave(T.p(f, 2) * 1.35) - elif f < T.rango(6)[0]: + # which combing is on and how combed it is + if f < T.span(2)[0]: + idx, combed = 0, 0.0 + elif f < T.span(4)[0]: + idx, combed = 0, suave(T.p(f, 2) * 1.35) + elif f < T.span(6)[0]: p4 = T.p(f, 4) idx = 0 if p4 < 0.35 else 1 - peinado = 1.0 - suave(p4 / 0.35) if p4 < 0.35 else suave((p4 - 0.35) / 0.5) - elif f < T.rango(7)[0]: - idx, peinado = 1, 1.0 - elif f < T.rango(8)[0]: # desfile de peinados + combed = 1.0 - suave(p4 / 0.35) if p4 < 0.35 else suave((p4 - 0.35) / 0.5) + elif f < T.span(7)[0]: + idx, combed = 1, 1.0 + elif f < T.span(8)[0]: # parade of combings q = T.p(f, 7) * 3.0 idx = 1 + min(3, int(q)) sub = q - int(q) - peinado = min(1.0, sub * 2.6) + combed = min(1.0, sub * 2.6) else: - idx, peinado = 4, 1.0 - peinar(idx, peinado) - - # marcas en los dos ceros del campo - eje = normaliza(PEINADOS[idx][0]) - vis = suave((T.p(f, 3) - 0.1) / 0.4) if f >= T.rango(3)[0] else 0.0 - if f >= T.rango(9)[0]: - vis *= 1.0 - suave(T.p(f, 9) / 0.4) - for k, g in enumerate(ob["marcas"]): + idx, combed = 4, 1.0 + comb(idx, combed) + + # marks on the two zeros of the field + axis_obj = normalize_text(COMBINGS[idx][0]) + visible = suave((T.p(f, 3) - 0.1) / 0.4) if f >= T.span(3)[0] else 0.0 + if f >= T.span(9)[0]: + visible *= 1.0 - suave(T.p(f, 9) / 0.4) + for k, g in enumerate(ob["tick_list"]): s = 1 if k == 0 else -1 - d = tuple(c * s for c in eje) - # el aro flota por encima del pelo, si no queda enterrado + d = tuple(c * s for c in axis_obj) + # the ring floats above the hair, otherwise it gets buried g.location = tuple(c * (R + LARGO * 0.62) for c in d) g.rotation_euler = Vector(d).to_track_quat('Z', 'Y').to_euler() - pul = 1.0 + 0.10 * math.sin(t * 4.2) - g.scale = (vis * pul,) * 3 + inch = 1.0 + 0.10 * math.sin(t * 4.2) + g.scale = (visible * inch,) * 3 - # el planeta: el pelo se vuelve viento + # the planet: the hair turns into wind pl = suave(T.p(f, 8)) - ob["piel"].node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = \ + ob["skin"].node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = \ (*tuple(a + (b - a) * pl for a, b in zip(srgb("azul"), srgb("#0E5A73"))), 1) - # la rosquilla entra en el beat 9 - ent = suave((T.p(f, 9) - 0.15) / 0.5) - sal = suave((T.p(f, 10) - 0.05) / 0.35) - s_esf = (1.0 - ent) + sal * 0.60 - ob["rig"].scale = (s_esf,) * 3 - ob["rig"].location = (0.0 - 1.00 * sal, 0.0, CZ + 0.22 * sal) - s_ros = ent * (1.0 - 0.38 * sal) - ob["rig_t"].scale = (s_ros,) * 3 - ob["rig_t"].location = (0.0 + 1.02 * sal, 0.0, CZ + 0.22 * sal) - - e = ob["etq"] + # the donut comes in at beat 9 + whole = suave((T.p(f, 9) - 0.15) / 0.5) + out_node = suave((T.p(f, 10) - 0.05) / 0.35) + s_sph = (1.0 - whole) + out_node * 0.60 + ob["rig"].scale = (s_sph,) * 3 + ob["rig"].location = (0.0 - 1.00 * out_node, 0.0, CZ + 0.22 * out_node) + s_donut = whole * (1.0 - 0.38 * out_node) + ob["rig_t"].scale = (s_donut,) * 3 + ob["rig_t"].location = (0.0 + 1.02 * out_node, 0.0, CZ + 0.22 * out_node) + + e = ob["tag_m"] e["esf"].location = (-1.02, 0.0, CZ - 0.98) e["ros"].location = (1.05, 0.0, CZ - 0.98) e["pie"].location = (0.0, 0.0, CZ - 1.42) for o in e.values(): - o.scale = (sal,) * 3 + o.scale = (out_node,) * 3 - render_secuencia(NOMBRE, T, actualizar) + render_sequence(NAME_KEY, T, refresh) main() -- cgit v1.2.3