diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 20:21:47 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 20:21:47 -0300 |
| commit | 59355909f2de9236af8168a26c70bcf6caa3b285 (patch) | |
| tree | 686186e2086f81aa22ad25e78eb29fca3cbadc9a /blender/bolapeluda.py | |
| download | 100cia-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.py | 260 |
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() |