# -*- 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()