aboutsummaryrefslogtreecommitdiffstats
path: root/blender
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
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')
-rw-r--r--blender/base.py595
-rw-r--r--blender/bloques.py306
-rw-r--r--blender/bolapeluda.py274
-rw-r--r--blender/caos.py228
-rw-r--r--blender/diferencial.py388
-rw-r--r--blender/domino.py462
-rw-r--r--blender/galton.py398
7 files changed, 1329 insertions, 1322 deletions
diff --git a/blender/base.py b/blender/base.py
index 385eb0c..74b23be 100644
--- a/blender/base.py
+++ b/blender/base.py
@@ -1,29 +1,36 @@
# -*- coding: utf-8 -*-
-"""Base comun para las escenas 3D del canal (Blender 5.2, EEVEE, headless).
+"""Common base for the channel's 3D scenes (Blender 5.2, EEVEE, headless).
-Idea central: no se usan keyframes. El render recorre los frames uno por uno y
-antes de cada uno llama a una funcion actualizar(f) que coloca todo. Asi la
-fisica y la geometria se calculan exactas en cada cuadro y no hay que pelear
-con la API de acciones por capas de Blender 5.x.
+Key idea: no keyframes are used. The render walks the frames one by one and
+calls a refresh(f) function before each one that places everything. That way
+physics and geometry are computed exactly on every frame and there is no
+fighting with the layered actions API of Blender 5.x.
-El tiempo sale de out/<nombre>_timeline.json, el mismo archivo que usa
-build_video.py para colocar el audio: la sincronia es por construccion.
+Timing comes from out/<name>_timeline.json, the same file build_video.py uses
+to place the audio: sync holds by construction.
"""
import json, math, os, sys
import bpy
-ROOT = os.environ.get("PROY", "/tmp/Firefox/videos-t")
+
+def env(name, old=None, default=None):
+ """Environment variable, also accepting its older Spanish name."""
+ return os.environ.get(name, os.environ.get(old, default) if old else default)
+
+
+# project root: defaults to the directory above this file
+ROOT = env("PROJECT", "PROY", os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
FPS = 30
W, H = 1080, 1920
-# --- paleta del canal (sRGB) --------------------------------------------------
+# --- channel palette (sRGB) ---------------------------------------------------
HEX = {"ambar": "FFD166", "verde": "06D6A0", "rosa": "EF476F", "celeste": "4CC9F0",
"gris": "8D99AE", "fondo": "0B0F1A", "azul": "1D3B6E", "riel": "1B2233",
"blanco": "FFFFFF", "punto": "243050"}
def srgb(h):
- """Hex sRGB -> lineal, que es lo que espera Blender en los materiales."""
+ """sRGB hex -> linear, which is what Blender expects in materials."""
def c(v):
v /= 255.0
return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
@@ -31,73 +38,73 @@ def srgb(h):
return tuple(c(int(h[i:i + 2], 16)) for i in (0, 2, 4))
-def escena(muestras=None):
- """Escena limpia, vertical, EEVEE, fondo transparente y color sin tonemap."""
+def scene_setup(samples=None):
+ """Clean vertical scene, EEVEE, transparent background and no tonemapping."""
bpy.ops.wm.read_factory_settings(use_empty=True)
sc = bpy.context.scene
sc.render.engine = 'BLENDER_EEVEE'
- sc.eevee.taa_render_samples = int(muestras or os.environ.get("MUESTRAS", 16))
+ sc.eevee.taa_render_samples = int(samples or env("SAMPLES", "MUESTRAS", 16))
sc.render.resolution_x, sc.render.resolution_y = W, H
sc.render.resolution_percentage = int(os.environ.get("PCT", 100))
sc.render.fps = FPS
- sc.render.film_transparent = True # el fondo punteado se compone aparte
+ sc.render.film_transparent = True # the dotted background is composited separately
sc.render.image_settings.file_format = 'PNG'
sc.render.image_settings.color_mode = 'RGBA'
sc.render.image_settings.compression = 25
- # 'Standard' deja los colores tal cual se escriben; AgX los lavaria y el
- # canal quedaria con otra paleta que los videos de Manim.
+ # 'Standard' keeps colors exactly as written; AgX would wash them out and the
+ # channel would end up with a different palette from the Manim videos.
for prop, val in (("view_transform", 'Standard'), ("look", 'None')):
try:
setattr(sc.view_settings, prop, val)
except Exception as e:
print(f" aviso: view_settings.{prop} -> {e}")
- mundo = bpy.data.worlds.new("mundo")
- mundo.use_nodes = True
- mundo.node_tree.nodes["Background"].inputs[0].default_value = (*srgb("fondo"), 1)
- mundo.node_tree.nodes["Background"].inputs[1].default_value = 0.45
- sc.world = mundo
+ world_obj = bpy.data.worlds.new("mundo")
+ world_obj.use_nodes = True
+ world_obj.node_tree.nodes["Background"].inputs[0].default_value = (*srgb("fondo"), 1)
+ world_obj.node_tree.nodes["Background"].inputs[1].default_value = 0.45
+ sc.world = world_obj
return sc
-def poner(nodo, nombre, valor):
- """setattr/socket tolerante: avisa en vez de romper si cambio el nombre."""
+def put(node, obj_name, valor):
+ """Tolerant setattr/socket: warns instead of breaking if a name changed."""
try:
- if nombre in nodo.inputs:
- nodo.inputs[nombre].default_value = valor
+ if obj_name in node.inputs:
+ node.inputs[obj_name].default_value = valor
return True
except Exception:
pass
- print(f" aviso: no existe la entrada '{nombre}'")
+ print(f" warning: input '{obj_name}' does not exist")
return False
-def material(nombre, color, rug=0.35, metal=0.0, emis=0.0, alpha=1.0):
- m = bpy.data.materials.new(nombre)
+def material(obj_name, color, rough=0.35, metal=0.0, emit=0.0, alpha=1.0):
+ m = bpy.data.materials.new(obj_name)
m.use_nodes = True
b = m.node_tree.nodes.get("Principled BSDF")
col = (*(color if isinstance(color, tuple) else srgb(color)), 1.0)
- poner(b, "Base Color", col)
- poner(b, "Roughness", rug)
- poner(b, "Metallic", metal)
- if emis:
- poner(b, "Emission Color", col)
- poner(b, "Emission Strength", emis)
+ put(b, "Base Color", col)
+ put(b, "Roughness", rough)
+ put(b, "Metallic", metal)
+ if emit:
+ put(b, "Emission Color", col)
+ put(b, "Emission Strength", emit)
if alpha < 1.0:
- poner(b, "Alpha", alpha)
+ put(b, "Alpha", alpha)
m.blend_method = 'BLEND' if hasattr(m, "blend_method") else m.blend_method
return m
-def objeto(nombre, malla, mat=None, coleccion=None):
- ob = bpy.data.objects.new(nombre, malla)
- (coleccion or bpy.context.collection).objects.link(ob)
+def make_object(obj_name, mesh_obj, mat=None, collection_obj=None):
+ ob = bpy.data.objects.new(obj_name, mesh_obj)
+ (collection_obj or bpy.context.collection).objects.link(ob)
if mat is not None:
ob.data.materials.append(mat)
return ob
-def malla_de(nombre, verts, faces, suave=True):
- me = bpy.data.meshes.new(nombre)
+def mesh_from(obj_name, verts, faces, suave=True):
+ me = bpy.data.meshes.new(obj_name)
me.from_pydata(verts, [], faces)
me.update()
if suave:
@@ -106,11 +113,11 @@ def malla_de(nombre, verts, faces, suave=True):
return me
-def curva_poly(nombre, splines, grosor=0.01, radios=None, mat=None):
- """Una curva con varias splines POLY; radios opcional por punto."""
- cu = bpy.data.curves.new(nombre, 'CURVE')
+def curve_poly(obj_name, splines, thickness_px=0.01, radios=None, mat=None):
+ """A curve with several POLY splines; optional per-point radii."""
+ cu = bpy.data.curves.new(obj_name, 'CURVE')
cu.dimensions = '3D'
- cu.bevel_depth = grosor
+ cu.bevel_depth = thickness_px
cu.bevel_resolution = 1
cu.use_fill_caps = True
for k, pts in enumerate(splines):
@@ -120,11 +127,11 @@ def curva_poly(nombre, splines, grosor=0.01, radios=None, mat=None):
sp.points[i].co = (p[0], p[1], p[2], 1.0)
if radios is not None:
sp.points[i].radius = radios[k][i]
- return objeto(nombre, cu, mat)
+ return make_object(obj_name, cu, mat)
-def rehacer_curva(ob, splines, radios=None):
- """Reescribe los puntos de una curva ya creada (misma cantidad de puntos)."""
+def rebuild_curve(ob, splines, radios=None):
+ """Rewrites the points of an existing curve (same number of points)."""
for sp, pts, k in zip(ob.data.splines, splines, range(len(splines))):
for i, p in enumerate(pts):
sp.points[i].co = (p[0], p[1], p[2], 1.0)
@@ -132,54 +139,54 @@ def rehacer_curva(ob, splines, radios=None):
sp.points[i].radius = radios[k][i]
-def camara(loc, mira=(0, 0, 0), lente=50):
+def camera_obj(loc, sight=(0, 0, 0), lens=50):
cd = bpy.data.cameras.new("cam")
- cd.lens = lente
+ cd.lens = lens
cam = bpy.data.objects.new("cam", cd)
bpy.context.collection.objects.link(cam)
bpy.context.scene.camera = cam
- apuntar(cam, loc, mira)
+ aim_at(cam, loc, sight)
return cam
-def apuntar(ob, loc, mira):
- """Coloca ob en loc mirando a 'mira' (convencion de camara: -Z adelante)."""
+def aim_at(ob, loc, sight):
+ """Places ob at loc looking at 'sight' (camera convention: -Z forward)."""
from mathutils import Vector
ob.location = loc
- d = Vector(mira) - Vector(loc)
+ d = Vector(sight) - Vector(loc)
ob.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler()
-def luz(nombre, tipo, loc, energia, color="blanco", tam=2.0, mira=None):
- ld = bpy.data.lights.new(nombre, tipo)
- ld.energy = energia
+def light_obj(obj_name, kind_m, loc, energy_val, color="blanco", size_u=2.0, sight=None):
+ ld = bpy.data.lights.new(obj_name, kind_m)
+ ld.energy = energy_val
ld.color = srgb(color)
- if tipo == 'AREA':
- ld.size = tam
- if tipo == 'POINT':
- ld.shadow_soft_size = tam
- ob = bpy.data.objects.new(nombre, ld)
+ if kind_m == 'AREA':
+ ld.size = size_u
+ if kind_m == 'POINT':
+ ld.shadow_soft_size = size_u
+ ob = bpy.data.objects.new(obj_name, ld)
bpy.context.collection.objects.link(ob)
- if mira is not None:
- apuntar(ob, loc, mira)
+ if sight is not None:
+ aim_at(ob, loc, sight)
else:
ob.location = loc
return ob
-# --- tiempo -------------------------------------------------------------------
-class Tiempo:
- """Traduce el timeline del canal a frames."""
+# --- timing -------------------------------------------------------------------
+class Timeline:
+ """Turns the channel timeline into frames."""
- def __init__(self, nombre):
- with open(os.path.join(ROOT, "out", f"{nombre}_timeline.json")) as f:
+ def __init__(self, obj_name):
+ with open(os.path.join(ROOT, "out", f"{obj_name}_timeline.json")) as f:
self.tl = json.load(f)
self.beats = self.tl["beats"]
self.total = self.tl["total"]
self.n_frames = int(round(self.total * FPS))
- def rango(self, i):
- """(frame inicial, frame final) del beat i, 1-based inclusive."""
+ def span(self, i):
+ """(first frame, last frame) of beat i, 1-based inclusive."""
b = self.beats[i]
a = int(round(b["start"] * FPS)) + 1
z = int(round((b["start"] + b["dur"]) * FPS))
@@ -189,8 +196,8 @@ class Tiempo:
return (f - 1) / FPS
def p(self, f, i):
- """Progreso 0..1 dentro del beat i (fuera del beat: 0 antes, 1 despues)."""
- a, z = self.rango(i)
+ """Progress 0..1 within beat i (outside the beat: 0 before, 1 after)."""
+ a, z = self.span(i)
if f <= a:
return 0.0
if f >= z:
@@ -203,111 +210,111 @@ def suave(x):
return x * x * (3 - 2 * x)
-def mezcla(a, b, x):
+def mix_m(a, b, x):
return a + (b - a) * suave(x)
# --- render -------------------------------------------------------------------
-def render_secuencia(nombre, tiempo, actualizar, desde=None, hasta=None):
+def render_sequence(obj_name, elapsed_t, refresh, since=None, until=None):
sc = bpy.context.scene
- carpeta = os.path.join(ROOT, "render", nombre)
- os.makedirs(carpeta, exist_ok=True)
- a = int(desde or os.environ.get("DESDE", 1))
- z = int(hasta or os.environ.get("HASTA", tiempo.n_frames))
- salto = os.environ.get("SALTO") # para pruebas: 1 de cada N
- frames = range(a, z + 1, int(salto) if salto else 1)
+ folder = os.path.join(ROOT, "render", obj_name)
+ os.makedirs(folder, exist_ok=True)
+ a = int(since or env("FROM_FRAME", "DESDE", 1))
+ z = int(until or env("TO_FRAME", "HASTA", elapsed_t.n_frames))
+ jump = env("EVERY", "SALTO") # for tests: 1 out of every N
+ frames = range(a, z + 1, int(jump) if jump else 1)
import time
t0 = time.time()
- hechos = 0
+ done = 0
for f in frames:
- destino = os.path.join(carpeta, f"f{f:04d}.png")
- if os.environ.get("SEGUIR") and os.path.exists(destino):
+ target = os.path.join(folder, f"f{f:04d}.png")
+ if env("RESUME", "SEGUIR") and os.path.exists(target):
continue
sc.frame_set(f)
- actualizar(f)
- sc.render.filepath = destino
+ refresh(f)
+ sc.render.filepath = target
bpy.ops.render.render(write_still=True)
- hechos += 1
- if hechos % 25 == 0:
+ done += 1
+ if done % 25 == 0:
d = time.time() - t0
- print(f" [{nombre}] frame {f}/{z} {d / hechos:.2f}s/frame "
- f"faltan {(len(frames) - hechos) * d / hechos / 60:.1f} min", flush=True)
- print(f"[{nombre}] {hechos} frames en {(time.time() - t0) / 60:.1f} min", flush=True)
+ print(f" [{obj_name}] frame {f}/{z} {d / done:.2f}s/frame "
+ f"faltan {(len(frames) - done) * d / done / 60:.1f} min", flush=True)
+ print(f"[{obj_name}] {done} frames in {(time.time() - t0) / 60:.1f} min", flush=True)
-# --- texto 3D -----------------------------------------------------------------
-FUENTE_RUTA = "/usr/share/fonts/TTF/Roboto-Bold.ttf"
-_fuente = None
+# --- 3D text -----------------------------------------------------------------
+FONT_PATH = "/usr/share/fonts/TTF/Roboto-Bold.ttf"
+_font = None
-def texto(cuerpo, tam=0.18, color="blanco", align='CENTER', emis=1.8, plano_xz=True):
- """Texto plano, emisivo, en el plano XZ (para camaras que miran por -Y)."""
- global _fuente
- if _fuente is None:
- _fuente = bpy.data.fonts.load(FUENTE_RUTA)
+def txt_m(body_obj, size_u=0.18, color="blanco", align='CENTER', emit=1.8, plano_xz=True):
+ """Flat emissive text on the XZ plane (for cameras looking along -Y)."""
+ global _font
+ if _font is None:
+ _font = bpy.data.fonts.load(FONT_PATH)
cu = bpy.data.curves.new("txt", 'FONT')
- cu.body = cuerpo
- cu.font = _fuente
- cu.size = tam
+ cu.body = body_obj
+ cu.font = _font
+ cu.size = size_u
cu.align_x = align
cu.align_y = 'CENTER'
- ob = objeto("txt", cu, material(f"m_txt_{color}", color, emis=emis, rug=0.6))
+ ob = make_object("txt", cu, material(f"m_txt_{color}", color, emit=emit, rough=0.6))
if plano_xz:
ob.rotation_euler = (math.pi / 2, 0, 0)
return ob
-def cilindro(nombre, radio, largo, mat=None, lados=20):
- """Cilindro centrado en el origen, eje +Z, para reubicar por frame."""
+def cylinder(obj_name, radio, largo, mat=None, sides=20):
+ """Cylinder centred on the origin, +Z axis, to be repositioned per frame."""
verts, faces = [], []
- for i in range(lados):
- a = 2 * math.pi * i / lados
+ for i in range(sides):
+ a = 2 * math.pi * i / sides
verts.append((radio * math.cos(a), radio * math.sin(a), -largo / 2))
verts.append((radio * math.cos(a), radio * math.sin(a), largo / 2))
- for i in range(lados):
- j = (i + 1) % lados
+ for i in range(sides):
+ j = (i + 1) % sides
faces.append((2 * i, 2 * j, 2 * j + 1, 2 * i + 1))
- tapa_a = [2 * i for i in range(lados)][::-1]
- tapa_b = [2 * i + 1 for i in range(lados)]
- faces += [tuple(tapa_a), tuple(tapa_b)]
- me = malla_de(nombre, verts, faces, suave=False)
- for p in me.polygons[:lados]:
+ lid_a = [2 * i for i in range(sides)][::-1]
+ lid_b = [2 * i + 1 for i in range(sides)]
+ faces += [tuple(lid_a), tuple(lid_b)]
+ me = mesh_from(obj_name, verts, faces, suave=False)
+ for p in me.polygons[:sides]:
p.use_smooth = True
- return objeto(nombre, me, mat)
+ return make_object(obj_name, me, mat)
-def esfera(nombre, radio, mat=None, seg=24, anillos=14):
+def sphere(obj_name, radio, mat=None, seg_m=24, rings=14):
verts, faces = [], []
- for i in range(1, anillos):
- phi = math.pi * i / anillos
- for j in range(seg):
- th = 2 * math.pi * j / seg
+ for i in range(1, rings):
+ phi = math.pi * i / rings
+ for j in range(seg_m):
+ th = 2 * math.pi * j / seg_m
verts.append((radio * math.sin(phi) * math.cos(th),
radio * math.sin(phi) * math.sin(th),
radio * math.cos(phi)))
- norte = len(verts); verts.append((0, 0, radio))
- sur = len(verts); verts.append((0, 0, -radio))
- for i in range(anillos - 2):
- for j in range(seg):
- a = i * seg + j
- b = i * seg + (j + 1) % seg
- faces.append((a, b, b + seg, a + seg))
- for j in range(seg):
- faces.append((norte, (j + 1) % seg, j))
- faces.append((sur, (anillos - 2) * seg + j, (anillos - 2) * seg + (j + 1) % seg))
- return objeto(nombre, malla_de(nombre, verts, faces), mat)
-
-
-def orientar(ob, desde, hasta):
- """Coloca un cilindro creado con cilindro() entre dos puntos."""
+ north = len(verts); verts.append((0, 0, radio))
+ south = len(verts); verts.append((0, 0, -radio))
+ for i in range(rings - 2):
+ for j in range(seg_m):
+ a = i * seg_m + j
+ b = i * seg_m + (j + 1) % seg_m
+ faces.append((a, b, b + seg_m, a + seg_m))
+ for j in range(seg_m):
+ faces.append((north, (j + 1) % seg_m, j))
+ faces.append((south, (rings - 2) * seg_m + j, (rings - 2) * seg_m + (j + 1) % seg_m))
+ return make_object(obj_name, mesh_from(obj_name, verts, faces), mat)
+
+
+def orient(ob, since, until):
+ """Places a cylinder created with cylinder() between two points."""
from mathutils import Vector
- a, b = Vector(desde), Vector(hasta)
+ a, b = Vector(since), Vector(until)
ob.location = (a + b) / 2
d = b - a
ob.rotation_euler = d.to_track_quat('Z', 'Y').to_euler()
-def toro(nombre, R, r, mat=None, u=64, v=24):
+def torus(obj_name, R, r, mat=None, u=64, v=24):
verts, faces = [], []
for i in range(u):
a = 2 * math.pi * i / u
@@ -321,38 +328,38 @@ def toro(nombre, R, r, mat=None, u=64, v=24):
for j in range(v):
j2 = (j + 1) % v
faces.append((i * v + j, i2 * v + j, i2 * v + j2, i * v + j2))
- return objeto(nombre, malla_de(nombre, verts, faces), mat)
+ return make_object(obj_name, mesh_from(obj_name, verts, faces), mat)
-HOLGURA = 0.55 # 0.5 = sin juego; un poco mas deja luz entre flancos
+SLACK = 0.55 # 0.5 = no play; a bit more leaves light between flanks
-def engranaje_conico(nombre, eje, N=18, gamma=45.0, d_i=0.50, d_o=0.70,
- alto=0.055, mat=None, nd=10, por_diente=10, fase=0.0,
- espiral=0.0, espesor=0.10, hueco=0.0):
- """Engranaje conico con el apice en el origen y eje 'eje'.
+def bevel_gear(obj_name, axis_obj, N=18, gamma=45.0, d_i=0.50, d_o=0.70,
+ alto=0.055, mat=None, nd=10, per_tooth=10, phase=0.0,
+ spiral=0.0, thickness=0.10, gap_m=0.0):
+ """Bevel gear with its apex at the origin and axis 'axis_obj'.
- gamma = semiangulo del cono de paso. Dos engranajes engranan a 90 grados
- cuando sus gammas suman 90: 45+45 para los del diferencial,
- 72+18 para corona y pinon.
- espiral = torsion del diente a lo largo de la generatriz (conico espiral,
- que es lo que se usa de verdad; en recto se ve de juguete).
- espesor = cuanto se extruye el cuerpo hacia atras, para que sea un solido
- y no una cascara.
+ gamma = half-angle of the pitch cone. Two gears mesh at 90 degrees
+ when their gammas add up to 90: 45+45 for the differential
+ ones, 72+18 for ring gear and pinion.
+ spiral = twist of the tooth along the generatrix (spiral bevel, which
+ is what is really used; straight teeth look like a toy).
+ thickness = how far the body is extruded backwards, so it is a solid and
+ not a shell.
"""
from mathutils import Vector
- a = Vector(eje).normalized()
+ a = Vector(axis_obj).normalized()
u = Vector((0, 0, 1)) if abs(a.z) < 0.9 else Vector((1, 0, 0))
u = (u - a * u.dot(a)).normalized()
v = a.cross(u)
cg, sg = math.cos(math.radians(gamma)), math.sin(math.radians(gamma))
- nphi = N * por_diente
+ nphi = N * per_tooth
verts, faces = [], []
- def perfil(x):
+ def profile(x):
return max(0.0, min(1.0, (math.cos(x) + 0.30) / 0.60))
- def punto(d, phi, h):
+ def point(d, phi, h):
rad = u * math.cos(phi) + v * math.sin(phi)
base = (a * cg + rad * sg) * d
nrm = rad * cg - a * sg
@@ -363,79 +370,79 @@ def engranaje_conico(nombre, eje, N=18, gamma=45.0, d_i=0.50, d_o=0.70,
d = d_i + (d_o - d_i) * s_
for j in range(nphi):
phi = 2 * math.pi * j / nphi
- # el diente arranca bajo en el extremo interior y crece hacia afuera
- # el diente se apaga en el borde interior: si no, el cierre del
- # cuerpo queda como un abanico de aletas
- # el diente va mitad por encima y mitad por debajo del cono de paso:
- # asi el de enfrente entra en el hueco en vez de atravesar el cuerpo
- h = alto * (perfil(N * phi + fase + espiral * s_) - HOLGURA) * (s_ ** 0.8)
- p = punto(d, phi, h)
+ # the tooth starts low at the inner end and grows outwards
+ # the tooth fades at the inner edge: otherwise the body closure
+ # looks like a fan of fins
+ # the tooth sits half above and half below the pitch cone:
+ # that way the opposite gear goes into the gap instead of through the body
+ h = alto * (profile(N * phi + phase + spiral * s_) - SLACK) * (s_ ** 0.8)
+ p = point(d, phi, h)
verts.append((p.x, p.y, p.z))
for i in range(nd):
for j in range(nphi):
j2 = (j + 1) % nphi
faces.append((i * nphi + j, i * nphi + j2, (i + 1) * nphi + j2, (i + 1) * nphi + j))
- # cuerpo: se extruye el borde exterior hacia atras y se cierra con el fondo
+ # body: the outer edge is extruded backwards and closed with the bottom
o_ext = nd * nphi
base_ext = len(verts)
for j in range(nphi):
phi = 2 * math.pi * j / nphi
- # se extruye desde el diente, no desde el cono: asi el perfil llega a la
- # cara exterior como en un engranaje de verdad
- h = alto * (perfil(N * phi + fase + espiral) - HOLGURA)
- # el cuerpo va del lado de atras (lejos del apice), no hacia el engranaje
- # de enfrente
- p = punto(d_o, phi, h) + a * espesor
+ # extruded from the tooth, not from the cone: that way the profile reaches
+ # the outer face like on a real gear
+ h = alto * (profile(N * phi + phase + spiral) - SLACK)
+ # the body goes on the back side (away from the apex), not towards the
+ # opposite gear
+ p = point(d_o, phi, h) + a * thickness
verts.append((p.x, p.y, p.z))
for j in range(nphi):
j2 = (j + 1) % nphi
faces.append((o_ext + j, o_ext + j2, base_ext + j2, base_ext + j))
base_int = len(verts)
- r_h = max(hueco, 0.02)
+ r_h = max(gap_m, 0.02)
for j in range(nphi):
phi = 2 * math.pi * j / nphi
rad = u * math.cos(phi) + v * math.sin(phi)
- p = rad * r_h + a * (d_i * cg) + a * espesor * 0.35
+ p = rad * r_h + a * (d_i * cg) + a * thickness * 0.35
verts.append((p.x, p.y, p.z))
for j in range(nphi):
j2 = (j + 1) % nphi
faces.append((base_ext + j, base_ext + j2, base_int + j2, base_int + j))
faces.append((j2, j, base_int + j, base_int + j2))
- me = malla_de(nombre, verts, faces, suave=False)
+ me = mesh_from(obj_name, verts, faces, suave=False)
import bmesh
bm = bmesh.new()
bm.from_mesh(me)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(me)
bm.free()
- ob = objeto(nombre, me, mat)
- # la cara dentada suave (es una superficie continua, como un engranaje
- # mecanizado); el cuerpo plano, para que se lean los cantos
+ ob = make_object(obj_name, me, mat)
+ # the toothed face smooth (it is a continuous surface, like a machined
+ # gear); the body flat, so the edges read
for k, pol in enumerate(me.polygons):
pol.use_smooth = k < nd * nphi
return ob
-def anillo_plano(nombre, r_int, r_ext, espesor, mat=None, lados=64):
- """Anillo macizo (brida, corona de tornillos, llanta)."""
+def flat_ring(obj_name, r_int, r_ext, thickness, mat=None, sides=64):
+ """Solid ring (flange, bolt circle, rim)."""
verts, faces = [], []
- for j in range(lados):
- ang = 2 * math.pi * j / lados
+ for j in range(sides):
+ ang = 2 * math.pi * j / sides
c, s_ = math.cos(ang), math.sin(ang)
- for (r, z) in ((r_int, -espesor / 2), (r_ext, -espesor / 2),
- (r_ext, espesor / 2), (r_int, espesor / 2)):
+ for (r, z) in ((r_int, -thickness / 2), (r_ext, -thickness / 2),
+ (r_ext, thickness / 2), (r_int, thickness / 2)):
verts.append((r * c, r * s_, z))
- for j in range(lados):
- j2 = (j + 1) % lados
+ for j in range(sides):
+ j2 = (j + 1) % sides
for k in range(4):
k2 = (k + 1) % 4
faces.append((j * 4 + k, j2 * 4 + k, j2 * 4 + k2, j * 4 + k2))
- return objeto(nombre, malla_de(nombre, verts, faces, suave=False), mat)
+ return make_object(obj_name, mesh_from(obj_name, verts, faces, suave=False), mat)
-def neumatico(nombre, R, r, mat=None, nu=72, nv=28, tacos=26, prof=0.055):
- """Toro con banda de rodadura: tacos y dos canales longitudinales."""
+def tire(obj_name, R, r, mat=None, nu=72, nv=28, lugs=26, depth=0.055):
+ """Torus with a tread: lugs and two longitudinal grooves."""
verts, faces = [], []
for i in range(nu):
aa = 2 * math.pi * i / nu
@@ -444,108 +451,108 @@ def neumatico(nombre, R, r, mat=None, nu=72, nv=28, tacos=26, prof=0.055):
b = 2 * math.pi * j / nv
cb, sb = math.cos(b), math.sin(b)
rr = r
- if abs(sb) < 0.72: # zona de rodadura
- taco = 0.5 + 0.5 * math.cos(tacos * aa + 3.0 * b)
- canal = 1.0 if abs(sb) > 0.22 and abs(sb) < 0.5 else 0.0
- rr -= prof * (0.45 * (taco < 0.45) + 0.55 * canal)
+ if abs(sb) < 0.72: # tread area
+ lug = 0.5 + 0.5 * math.cos(lugs * aa + 3.0 * b)
+ groove = 1.0 if abs(sb) > 0.22 and abs(sb) < 0.5 else 0.0
+ rr -= depth * (0.45 * (lug < 0.45) + 0.55 * groove)
verts.append(((R + rr * cb) * ca, (R + rr * cb) * sa, rr * sb))
for i in range(nu):
i2 = (i + 1) % nu
for j in range(nv):
j2 = (j + 1) % nv
faces.append((i * nv + j, i2 * nv + j, i2 * nv + j2, i * nv + j2))
- return objeto(nombre, malla_de(nombre, verts, faces), mat)
+ return make_object(obj_name, mesh_from(obj_name, verts, faces), mat)
-def mundo_estudio(fuerza=0.55, arriba="#8FA6C4", abajo="#10141F"):
- """Gradiente de horizonte en el mundo. No se ve (el film es transparente)
- pero es lo que el metal refleja: sin esto el acero queda plano."""
+def studio_world(force=0.55, above="#8FA6C4", below="#10141F"):
+ """Horizon gradient in the world. It is not visible (the film is transparent)
+ but it is what metal reflects: without it steel looks flat."""
w = bpy.data.worlds.new("estudio")
w.use_nodes = True
nt = w.node_tree
for n in list(nt.nodes):
if n.type != 'OUTPUT_WORLD':
nt.nodes.remove(n)
- sal = next(n for n in nt.nodes if n.type == 'OUTPUT_WORLD')
- fondo = nt.nodes.new("ShaderNodeBackground")
+ out_node = next(n for n in nt.nodes if n.type == 'OUTPUT_WORLD')
+ background = nt.nodes.new("ShaderNodeBackground")
geo = nt.nodes.new("ShaderNodeNewGeometry")
sep = nt.nodes.new("ShaderNodeSeparateXYZ")
- mapa = nt.nodes.new("ShaderNodeMapRange")
- rampa = nt.nodes.new("ShaderNodeValToRGB")
- mapa.inputs[1].default_value = -0.45
- mapa.inputs[2].default_value = 0.85
- rampa.color_ramp.elements[0].color = (*srgb(abajo), 1)
- rampa.color_ramp.elements[1].color = (*srgb(arriba), 1)
- rampa.color_ramp.elements[0].position = 0.15
- rampa.color_ramp.elements[1].position = 0.92
+ map_obj = nt.nodes.new("ShaderNodeMapRange")
+ ramp = nt.nodes.new("ShaderNodeValToRGB")
+ map_obj.inputs[1].default_value = -0.45
+ map_obj.inputs[2].default_value = 0.85
+ ramp.color_ramp.elements[0].color = (*srgb(below), 1)
+ ramp.color_ramp.elements[1].color = (*srgb(above), 1)
+ ramp.color_ramp.elements[0].position = 0.15
+ ramp.color_ramp.elements[1].position = 0.92
nt.links.new(geo.outputs["Incoming"], sep.inputs[0])
- nt.links.new(sep.outputs["Z"], mapa.inputs[0])
- nt.links.new(mapa.outputs[0], rampa.inputs[0])
- nt.links.new(rampa.outputs["Color"], fondo.inputs[0])
- fondo.inputs[1].default_value = fuerza
- nt.links.new(fondo.outputs[0], sal.inputs[0])
+ nt.links.new(sep.outputs["Z"], map_obj.inputs[0])
+ nt.links.new(map_obj.outputs[0], ramp.inputs[0])
+ nt.links.new(ramp.outputs["Color"], background.inputs[0])
+ background.inputs[1].default_value = force
+ nt.links.new(background.outputs[0], out_node.inputs[0])
bpy.context.scene.world = w
return w
-def metal(nombre, color="#B9C0CC", rug=0.26, met=1.0):
- return material(nombre, color, rug=rug, metal=met)
+def metal(obj_name, color="#B9C0CC", rough=0.26, met=1.0):
+ return material(obj_name, color, rough=rough, metal=met)
-def rueda_completa(nombre, mats, R=0.36, r=0.135, ancho=0.20, radios=5):
- """Neumatico con tacos + llanta de aleacion con radios + disco de freno.
- Devuelve el grupo, con el eje de giro en X."""
- g = bpy.data.objects.new(nombre, None)
+def full_wheel(obj_name, mats, R=0.36, r=0.135, width_px=0.20, radios=5):
+ """Tire with lugs + alloy rim with spokes + brake disc.
+ Returns the group, with the spin axis along X."""
+ g = bpy.data.objects.new(obj_name, None)
bpy.context.collection.objects.link(g)
- goma = neumatico(f"{nombre}_goma", R, r, mats["goma"])
- goma.rotation_euler = (0, math.pi / 2, 0)
- goma.parent = g
+ rubber = tire(f"{obj_name}_goma", R, r, mats["goma"])
+ rubber.rotation_euler = (0, math.pi / 2, 0)
+ rubber.parent = g
r_int = R - r * 0.72
- aro = anillo_plano(f"{nombre}_aro", r_int - 0.045, r_int + 0.02, ancho, mats["alu"])
- aro.rotation_euler = (0, math.pi / 2, 0)
- aro.parent = g
- cubo = cilindro(f"{nombre}_cubo", 0.085, ancho * 0.9, mats["alu"], lados=24)
- cubo.rotation_euler = (0, math.pi / 2, 0)
- cubo.parent = g
+ hoop = flat_ring(f"{obj_name}_aro", r_int - 0.045, r_int + 0.02, width_px, mats["alu"])
+ hoop.rotation_euler = (0, math.pi / 2, 0)
+ hoop.parent = g
+ cube = cylinder(f"{obj_name}_cubo", 0.085, width_px * 0.9, mats["alu"], sides=24)
+ cube.rotation_euler = (0, math.pi / 2, 0)
+ cube.parent = g
for k in range(radios):
ang = 2 * math.pi * k / radios
d = (0.0, math.cos(ang), math.sin(ang))
- rad = cilindro(f"{nombre}_r{k}", 0.040, 1.0, mats["alu"], lados=12)
- orientar(rad, tuple(c * 0.075 for c in d), tuple(c * (r_int - 0.02) for c in d))
+ rad = cylinder(f"{obj_name}_r{k}", 0.040, 1.0, mats["alu"], sides=12)
+ orient(rad, tuple(c * 0.075 for c in d), tuple(c * (r_int - 0.02) for c in d))
rad.scale = (1, 1, (r_int - 0.02) - 0.075)
rad.parent = g
- disco = cilindro(f"{nombre}_disco", r_int - 0.09, 0.035, mats["freno"], lados=40)
+ disco = cylinder(f"{obj_name}_disco", r_int - 0.09, 0.035, mats["freno"], sides=40)
disco.rotation_euler = (0, math.pi / 2, 0)
- disco.location = (-ancho * 0.55, 0, 0)
+ disco.location = (-width_px * 0.55, 0, 0)
disco.parent = g
return g
-def tornillos(nombre, n, radio, eje_x, largo=0.05, r_t=0.028, mat=None, padre=None):
- """Corona de bulones sobre una brida."""
- g = bpy.data.objects.new(nombre, None)
+def screws(obj_name, n, radio, axis_x, largo=0.05, r_t=0.028, mat=None, padre=None):
+ """Bolt circle on a flange."""
+ g = bpy.data.objects.new(obj_name, None)
bpy.context.collection.objects.link(g)
for k in range(n):
ang = 2 * math.pi * k / n
- t = cilindro(f"{nombre}_{k}", r_t, largo, mat, lados=8)
+ t = cylinder(f"{obj_name}_{k}", r_t, largo, mat, sides=8)
t.rotation_euler = (0, math.pi / 2, 0)
- t.location = (eje_x, radio * math.cos(ang), radio * math.sin(ang))
+ t.location = (axis_x, radio * math.cos(ang), radio * math.sin(ang))
t.parent = g
if padre:
g.parent = padre
return g
-def sector_anillo(nombre, r_int, r_ext, espesor, ang0, ang1, mat=None, lados=28):
- """Pared curva: un sector de corona, con espesor a lo largo de su eje.
- Sirve para armar una caja de diferencial con ventanas."""
+def ring_sector(obj_name, r_int, r_ext, thickness, ang0, ang1, mat=None, sides=28):
+ """Curved wall: a ring sector with thickness along its axis.
+ Used to build a differential housing with windows."""
verts, faces = [], []
- n = max(3, lados)
+ n = max(3, sides)
for j in range(n + 1):
ang = ang0 + (ang1 - ang0) * j / n
c, s_ = math.cos(ang), math.sin(ang)
- for (r, z) in ((r_int, -espesor / 2), (r_ext, -espesor / 2),
- (r_ext, espesor / 2), (r_int, espesor / 2)):
+ for (r, z) in ((r_int, -thickness / 2), (r_ext, -thickness / 2),
+ (r_ext, thickness / 2), (r_int, thickness / 2)):
verts.append((r * c, r * s_, z))
for j in range(n):
for k in range(4):
@@ -554,106 +561,106 @@ def sector_anillo(nombre, r_int, r_ext, espesor, ang0, ang1, mat=None, lados=28)
faces.append((0, 3, 2, 1))
o = n * 4
faces.append((o, o + 1, o + 2, o + 3))
- return objeto(nombre, malla_de(nombre, verts, faces, suave=False), mat)
+ return make_object(obj_name, mesh_from(obj_name, verts, faces, suave=False), mat)
# --- madera procedural ----------------------------------------------------------
-def madera(nombre, claro="#9A6A40", oscuro="#4E2E17", escala=4.0, rug=0.42, veta=(1, 1, 9)):
- """Veta de madera: ondas distorsionadas por ruido, estiradas en un eje."""
- m = bpy.data.materials.new(nombre)
+def madera(obj_name, light_c="#9A6A40", dark="#4E2E17", scale_to=4.0, rough=0.42, grain=(1, 1, 9)):
+ """Wood grain: waves distorted by noise, stretched along one axis."""
+ m = bpy.data.materials.new(obj_name)
m.use_nodes = True
nt = m.node_tree
b = nt.nodes.get("Principled BSDF")
tc = nt.nodes.new("ShaderNodeTexCoord")
mp = nt.nodes.new("ShaderNodeMapping")
- mp.inputs["Scale"].default_value = veta
- ola = nt.nodes.new("ShaderNodeTexWave")
- ola.wave_type = 'RINGS'
- ola.inputs["Scale"].default_value = escala
- ola.inputs["Distortion"].default_value = 7.0
- ola.inputs["Detail"].default_value = 4.0
- ola.inputs["Detail Scale"].default_value = 1.6
- rampa = nt.nodes.new("ShaderNodeValToRGB")
- rampa.color_ramp.elements[0].color = (*srgb(oscuro), 1)
- rampa.color_ramp.elements[1].color = (*srgb(claro), 1)
- rampa.color_ramp.elements[0].position = 0.25
- rampa.color_ramp.elements[1].position = 0.85
+ mp.inputs["Scale"].default_value = grain
+ wave_tex = nt.nodes.new("ShaderNodeTexWave")
+ wave_tex.wave_type = 'RINGS'
+ wave_tex.inputs["Scale"].default_value = scale_to
+ wave_tex.inputs["Distortion"].default_value = 7.0
+ wave_tex.inputs["Detail"].default_value = 4.0
+ wave_tex.inputs["Detail Scale"].default_value = 1.6
+ ramp = nt.nodes.new("ShaderNodeValToRGB")
+ ramp.color_ramp.elements[0].color = (*srgb(dark), 1)
+ ramp.color_ramp.elements[1].color = (*srgb(light_c), 1)
+ ramp.color_ramp.elements[0].position = 0.25
+ ramp.color_ramp.elements[1].position = 0.85
nt.links.new(tc.outputs["Object"], mp.inputs["Vector"])
- nt.links.new(mp.outputs["Vector"], ola.inputs["Vector"])
- nt.links.new(ola.outputs["Fac"], rampa.inputs["Fac"])
- nt.links.new(rampa.outputs["Color"], b.inputs["Base Color"])
- poner(b, "Roughness", rug)
+ nt.links.new(mp.outputs["Vector"], wave_tex.inputs["Vector"])
+ nt.links.new(wave_tex.outputs["Fac"], ramp.inputs["Fac"])
+ nt.links.new(ramp.outputs["Color"], b.inputs["Base Color"])
+ put(b, "Roughness", rough)
bump = nt.nodes.new("ShaderNodeBump")
bump.inputs["Strength"].default_value = 0.08
- nt.links.new(ola.outputs["Fac"], bump.inputs["Height"])
+ nt.links.new(wave_tex.outputs["Fac"], bump.inputs["Height"])
nt.links.new(bump.outputs["Normal"], b.inputs["Normal"])
return m
-def caja(nombre, tam, loc=(0, 0, 0), mat=None, rot=(0, 0, 0)):
- """Prisma c