# -*- coding: utf-8 -*- """BLOQUES - the harmonic stack that overhangs the table without limit. Blender designs (table, floor, wooden blocks) and exports to glTF; Godot (Jolt) decides what holds and what falls. There are three experiments, each in its own lane of the table (different Y) so they do not touch; when rendering, the current one is brought to the front and the others hidden. A: the "obvious" staircase, each block shifted half a length -> falls B: 5 harmonic blocks (1/2, 1/4, 1/6, 1/8, 1/10) -> holds C: 16 harmonic blocks; at beat 8 one more is put on top -> all fall The harmonic shifts are scaled by SHIFT_SCALE = 0.95: that way the centre of mass of each sub-stack stays (1-C)/2 = 2.5 % of a length inside the edge holding it, on every level. Without that margin, the solver decides at random. """ import math, os, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import bpy from base import * NAME_KEY = "bloques" L, H, P = 1.0, 0.16, 0.50 # block length, height, depth SHIFT_SCALE = 0.95 C_TOWER = float(os.environ.get("T_C", SHIFT_SCALE)) # the tall tower needs more margin with the solver Z_FLOOR = -3.5 # 70 cm table with 20 cm blocks GRAVITY = 49.0 # 9.81 / 0.2: real 20 cm blocks LANE = {"a": 0.0, "b": 3.0, "c": 6.0, "x": 6.0} N_B, N_C = 5, 16 DX_EXTRA = float(os.environ.get("T_DX", -0.45)) # where the extra block rests def block_stack(n, c=SHIFT_SCALE): """Centre x of a harmonic stack of n blocks (index 0 = the top one).""" xs = [0.0] * n x = -L / 2 # the table acts as block n+1 for k in range(n, 0, -1): x += c * L / (2 * k) xs[k - 1] = x return xs def z_de(n, k): """Height of the centre of block k (0 = top) in a stack of n.""" return (n - 1 - k) * H + H / 2 def build_scene(): scene_setup(samples=int(env("SAMPLES", "MUESTRAS", 24))) studio_world(force=0.55) M = { "bloque": madera("bloque", light_c="#E2B880", dark="#B07A45", scale_to=5.0, rough=0.5), "arriba": material("arriba", "ambar", rough=0.35), "mesa": madera("mesa", light_c="#6B4426", dark="#2E1A0C", scale_to=2.5, rough=0.35, grain=(0.4, 1, 1)), "piso": madera("piso", light_c="#3A3F4B", dark="#1E222B", scale_to=1.5, rough=0.6, grain=(0.3, 1, 1)), "invisible": material("inv", "gris"), } phys = [] table = box_obj("box_mesa", (6.0, 9.0, 0.3), (-3.0, 3.0, -0.15), M["invisible"]) floor_obj = box_obj("box_piso", (30.0, 30.0, 0.2), (0.0, 3.0, Z_FLOOR - 0.1), M["invisible"]) table.hide_render = floor_obj.hide_render = True phys += [table, floor_obj] # what shows of the table and the floor: only the front lane box_obj("mesa_v", (6.0, 2.4, 0.3), (-3.0, 0.4, -0.15), M["mesa"]) m_leg = metal("pata", "#2A2F3A", rough=0.4) box_obj("pata", (0.14, 0.14, -Z_FLOOR - 0.3), (-0.35, -0.55, (Z_FLOOR - 0.3) / 2), m_leg) box_obj("pata2", (0.14, 0.14, -Z_FLOOR - 0.3), (-0.35, 1.3, (Z_FLOOR - 0.3) / 2), m_leg) box_obj("piso_v", (14.0, 5.0, 0.2), (1.0, 1.0, Z_FLOOR - 0.1), M["piso"]) bl = {} # A: "obvious" staircase for k in range(5): x = -0.25 + 0.5 * (4 - k) bl[f"a{k}"] = box_obj(f"block_a{k}", (L, P, H), (x, LANE["a"], z_de(5, k)), M["bloque"]) # B: harmonic stack of 5 for k, x in enumerate(block_stack(N_B)): bl[f"b{k}"] = box_obj(f"block_b{k}", (L, P, H), (x, LANE["b"], z_de(N_B, k)), M["arriba"] if k == 0 else M["bloque"]) # C: harmonic stack of 16 and the extra block, waiting on top xc = block_stack(N_C, C_TOWER) for k, x in enumerate(xc): bl[f"c{k:02d}"] = box_obj(f"block_c{k:02d}", (L, P, H), (x, LANE["c"], z_de(N_C, k)), M["arriba"] if k == 0 else M["bloque"]) bl["x"] = box_obj("block_x", (L, P, H), (xc[0] + DX_EXTRA, LANE["x"], z_de(N_C, 0) + H + 0.005), M["bloque"]) phys += list(bl.values()) return dict(phys=phys, bl=bl, M=M) def events(T): t = lambda f: (f - 1) / FPS fa = T.span(1)[0] + int(0.55 * (T.span(1)[1] - T.span(1)[0])) fx = T.span(8)[0] + int(0.70 * (T.span(8)[1] - T.span(8)[0])) return {"a": fa, "b": T.span(4)[0], "c": T.span(7)[0], "x": fx}, [ {"t": t(fa), "action": "release", "prefix": "block_a"}, {"t": t(T.span(4)[0]), "action": "release", "prefix": "block_b"}, {"t": t(T.span(7)[0]), "action": "release", "prefix": "block_c"}, {"t": t(fx), "action": "release", "prefix": "block_x"}, ] def simulate(T, obj): _, ev = events(T) cfg = { "duration": T.n_frames / FPS + 0.2, "gravity": GRAVITY, "scale": float(env("T_SCALE", None, 5.0)), "hz": int(os.environ.get("T_HZ", 1920)), "frozen": ["block_"], "rules": { "block_": {"friction": 0.55, "bounce": 0.08, "density": 600}, "box_": {"friction": 0.6, "bounce": 0.05}, }, "events": ev, } run_godot(export_physics(NAME_KEY, obj["phys"], cfg)) def main(): T = Timeline(NAME_KEY) obj = build_scene() if env("MODE", "MODO") == "sim": simulate(T, obj) return idx, D = load_sim(NAME_KEY) bl, M = obj["bl"], obj["M"] release_at, _ = events(T) xc = block_stack(N_C, C_TOWER) # report: what is still standing for g, n in (("a", 5), ("b", N_B), ("c", N_C)): nm_key = [k for k in bl if k.startswith(g) and k != "x"] zmin = min(D[-1, idx[bl[k].name], 2] for k in nm_key) zmin_before = min(D[min(len(D) - 1, release_at["x"] - 5), idx[bl[k].name], 2] for k in nm_key) print(f"[{NAME_KEY}] stack {g}: minimum z at the end {zmin:+.2f} " f"(before the extra block {zmin_before:+.2f})") print(f"[{NAME_KEY}] B: the top one starts at x={block_stack(N_B)[0] - L / 2:+.3f} " f"(overhangs {block_stack(N_B)[0] + L / 2:.3f}); C overhangs {xc[0] + L / 2:.3f}") # --- drawings ------------------------------------------------------------- m_edge = material("borde", "celeste", emit=2.5) edge = curve_poly("borde", [[(0, -0.30, z0), (0, -0.30, z0 + 0.09)] for z0 in [0.02 + 0.16 * i for i in range(18)]], thickness_px=0.012, mat=m_edge) label_list = [] xb = block_stack(N_B) for k in range(N_B): num = txt_m(f"1/{2 * (k + 1)}", size_u=0.15, color="ambar") # shift of this block relative to the one below, to its right num.location = (xb[k] + L / 2 + 0.16, -0.30, z_de(N_B, k)) label_list.append(num) air = curve_poly("aire", [[(0, -0.3, 0.9), (xb[0] - L / 2, -0.3, 0.9)]], thickness_px=0.02, mat=material("aire", "rosa", emit=3.0)) air_t = txt_m("en el aire", size_u=0.16, color="rosa") air_t.location = (0.45, -0.3, 1.08) # centres of mass of each sub-stack, over the edge that holds it cms = [] for k in range(1, N_B + 1): x_cm = sum(xb[:k]) / k e = sphere(f"cm{k}", 0.045, material(f"mcm{k}", "ambar", emit=3.0)) e.location = (x_cm, -0.30, z_de(N_B, k - 1) - H / 2) cms.append(e) count = txt_m("16 bloques", size_u=0.22, color="blanco") onto = txt_m("sobresale 1,6 bloques", size_u=0.18, color="ambar") ruler = curve_poly("regla", [[(0, -0.3, 2.78), (xc[0] + L / 2, -0.3, 2.78)]], thickness_px=0.018, mat=material("regla", "ambar", emit=3.0)) lens = 50.0 cam = camera_obj((0, -8, 1), (0, 0, 0), lens=lens) cam.data.sensor_fit = 'VERTICAL' cam.data.sensor_height = 36.0 light_obj("key", 'AREA', (-3.0, -4.0, 5.0), 1400, "blanco", size_u=4.0, sight=(0.5, 0, 0.8)) light_obj("fill", 'AREA', (4.0, -3.0, 0.5), 350, "blanco", size_u=4.0, sight=(0.5, 0, 0.5)) light_obj("rim", 'AREA', (1.0, 4.0, 4.0), 600, "blanco", size_u=3.0, sight=(0.5, 0, 1.0)) # (frame, centre x, content z, visible height) r = T.span tomas = [ (1, 0.55, 0.40, 5.6), (release_at["a"] + 5, 0.55, 0.40, 5.6), (r(1)[1] + 10, 0.9, -1.7, 7.4), (r(2)[0] + 18, 0.45, 0.45, 4.8), (r(5)[1], 0.45, 0.45, 4.8), (r(6)[0] + 10, 0.50, 0.9, 5.6), (r(6)[1], 0.55, 1.35, 6.4), (r(8)[1] - 4, 0.55, 1.35, 6.4), (r(9)[0] + 30, 1.2, -1.3, 8.4), (T.n_frames, 1.2, -1.4, 8.6), ] def camera_at(f): for (f0, *a), (f1, *b) in zip(tomas, tomas[1:]): if f0 <= f <= f1: u = suave((f - f0) / max(1, f1 - f0)) return [p + (q - p) * u for p, q in zip(a, b)] return tomas[-1][1:] def n_c(f): """How many blocks stack C has while it is being built (beat 6).""" return 5 + (N_C - 5) * suave(T.p(f, 6) / 0.8) def refresh(f): i = min(f - 1, len(D) - 1) group_m = "a" if f < r(2)[0] + 10 else "b" if f < r(6)[0] else "c" for k, ob in bl.items(): g = "x" if k == "x" else k[0] visible = (g == group_m) or (g == "x" and group_m == "c") ob.hide_render = not visible if not visible: continue row = list(D[i, idx[ob.name]]) row[1] -= LANE[g] set_pose(ob, row) if g == "a" and f < release_at["a"]: # from the neat stack to the staircase, before releasing it kk = int(k[1:]) u = suave(T.p(f, 1) / 0.45) ob.location.x = mix_m(-0.45, row[0], u) elif g == "b" and f < release_at["b"]: # built from the top: each block comes in from the left kk = int(k[1:]) u = suave((T.p(f, 3) - kk * 0.17) / 0.16) if f >= r(3)[0] else 0.0 if f < r(3)[0]: u = suave(T.p(f, 2) / 0.6) if kk == 0 else 0.0 ob.location.x = mix_m(row[0] - 3.5, row[0], u) ob.hide_render = u <= 0.001 elif g == "c" and f < release_at["c"]: kk = int(k[1:]) n = n_c(f) n0 = int(math.floor(n)) u = n - n0 def pos(nn): if kk >= nn: return None return block_stack(nn, C_TOWER)[kk], z_de(nn, kk) p0, p1 = pos(n0), pos(min(N_C, n0 + 1)) if p0 is None and p1 is None: ob.hide_render = True continue if p0 is None: # the new block comes in from below on the left p0 = (p1[0] - 3.0, p1[1]) x = p0[0] + (p1[0] - p0[0]) * u if p1 else p0[0] z = p0[1] + (p1[1] - p0[1]) * u if p1 else p0[1] ob.location = (x, 0.0, z) elif g == "x" and f < release_at["x"]: u = suave((T.p(f, 8) - 0.15) / 0.45) ob.hide_render = u <= 0.001 ob.location.z = row[2] + 2.0 * (1 - u) # drawings v_edge = T.p(f, 3) > 0 and f < r(9)[0] edge.hide_render = not v_edge for k, t in enumerate(label_list): t.hide_render = not (r(3)[0] + (k * 0.17 + 0.08) * (r(3)[1] - r(3)[0]) <= f < r(6)[0]) v_air = r(4)[0] + 20 <= f < r(6)[0] air.hide_render = air_t.hide_render = not v_air for k, e in enumerate(cms): e.hide_render = not (r(5)[0] + k * 8 <= f < r(6)[0]) v_c = r(6)[0] + 5 <= f < r(8)[1] count.hide_render = not v_c n = n_c(f) count.data.body = f"{int(round(n))} bloques" top_z = z_de(int(round(n)), 0) + H / 2 count.location = (-0.45, -0.3, top_z + 0.35) v_ruler = r(7)[0] <= f < r(8)[1] ruler.hide_render = onto.hide_render = not v_ruler onto.location = (1.2, -0.3, 2.98) cx, cz, h = camera_at(f) dist = h * lens / 36.0 zc = cz - 0.10 * h # the content stays above the subtitles aim_at(cam, (cx - 0.16 * h, -dist, zc + 0.16 * h), (cx, 0, zc)) render_sequence(NAME_KEY, T, refresh) main()