aboutsummaryrefslogtreecommitdiffstats
path: root/blender/bolapeluda.py
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 20:21:47 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 20:21:47 -0300
commit59355909f2de9236af8168a26c70bcf6caa3b285 (patch)
tree686186e2086f81aa22ad25e78eb29fca3cbadc9a /blender/bolapeluda.py
download100cia-videos-59355909f2de9236af8168a26c70bcf6caa3b285.tar.gz
100cia-videos-59355909f2de9236af8168a26c70bcf6caa3b285.zip
Import video pipeline as it was
Diffstat (limited to 'blender/bolapeluda.py')
-rw-r--r--blender/bolapeluda.py260
1 files changed, 260 insertions, 0 deletions
diff --git a/blender/bolapeluda.py b/blender/bolapeluda.py
new file mode 100644
index 0000000..a260784
--- /dev/null
+++ b/blender/bolapeluda.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+"""BOLA PELUDA - el teorema de la bola peluda (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.
+"""
+import math, os, sys
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+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"),
+ ((-0.58, -0.05, 0.82), "mer"),
+ ((0.97, -0.08, -0.22), "rot"),
+ ((0.12, -0.05, 0.99), "mer"),
+ ((-0.86, -0.06, -0.50), "rot"),
+ ((0.80, -0.05, 0.60), "rot")]
+
+
+def normaliza(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):
+ 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."""
+ pts, ga = [], math.pi * (3 - math.sqrt(5))
+ for i in range(n):
+ z = 1 - 2 * (i + 0.5) / n
+ r = math.sqrt(max(0.0, 1 - z * z))
+ th = ga * i
+ pts.append((r * math.cos(th), r * math.sin(th), z))
+ 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 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)
+ 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)
+ pos = [tuple(c * radio for c in p)]
+ paso = 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))
+ 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."""
+ splines, radios = [], []
+ for i in range(nu):
+ u = 2 * math.pi * i / nu
+ cu, su = math.cos(u), math.sin(u)
+ for j in range(nv):
+ w = 2 * math.pi * j / nv
+ 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
+ pos = [p]
+ paso = 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))
+ 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)
+ 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))
+
+ 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 = []
+ 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)
+
+ # la rosquilla, peinada de una
+ sp_t, ra_t = pelos_toro()
+ 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
+ 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():
+ 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)
+
+
+def main():
+ T = Tiempo(NOMBRE)
+ ob = construir(T)
+ base, pel_a, pel_b = ob["base"], ob["pel_a"], ob["pel_b"]
+ cache = {"clave": None}
+
+ def peinar(idx_eje, peinado):
+ clave = (idx_eje, round(peinado, 3))
+ if cache["clave"] == clave:
+ return
+ cache["clave"] = clave
+ eje = normaliza(PEINADOS[idx_eje][0])
+ tipo = PEINADOS[idx_eje][1]
+ sp, ra_a, ra_b = [], [], []
+ for p in base:
+ pos, m = pelo(p, eje, peinado, tipo)
+ 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)
+
+ def actualizar(f):
+ t = T.t(f)
+ # se mece en vez de girar: asi los dos ceros no se van al fondo
+ 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]:
+ 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
+ q = T.p(f, 7) * 3.0
+ idx = 1 + min(3, int(q))
+ sub = q - int(q)
+ peinado = 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"]):
+ 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
+ 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
+
+ # el planeta: el pelo se vuelve viento
+ pl = suave(T.p(f, 8))
+ ob["piel"].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"]
+ 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
+
+ render_secuencia(NOMBRE, T, actualizar)
+
+
+main()