# -*- coding: utf-8 -*- """DIFERENCIAL - why the two wheels can turn at different speeds. Every part is generated in code and moves with the exact kinematics of the mechanism: w_left + w_right = 2 * w_carrier, and the spider gear spins about its own axis at (w_right - w_left)/2 relative to the carrier. Pinion and ring gear keep their ratio: 14 and 43 teeth, with cone half-angles adding up to 90 degrees.""" import math, os, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import bpy from mathutils import Vector from base import * NAME_KEY = "diferencial" AXIS_X = 1.32 # where the wheels go R_WHEEL = 0.52 OMEGA = 1.45 # rad/s of the carrier RADIO_GIRO = 3.9 TRACK_W = 1.70 INCL = -0.72 # strongly diagonal: in 9:16 that is what uses the height N_COR, N_PIN = 43, 14 # 3.07:1 ratio, like a road differential G_COR, G_PIN = 72.0, 18.0 # the half-angles add up to 90: that is the meshing condition N_LAT = 16 X_COR = -0.50 # common apex of ring gear and pinion: far from the spider gears D_I, D_O = 0.70, 1.00 # the toothed face, the same on both P_PHASE = math.pi / 2 # so the tooth lands in the gap P_ESP = -0.62 # opposite hand to the ring gear P_NEXT = 1.0 # the assembly is 3.68 from end to end; tilted 41 degrees that is 2.77 wide # and 2.41 high, which is what sets these numbers FRAMING = [(0, 6.2), (2, 5.0), (3, 4.2), (4, 5.6), (5, 4.6), (6, 5.0), (7, 5.6), (8, 5.6), (9, 5.8), (10, 5.2)] DIR_CAM = Vector((0.30, -0.93, 0.21)).normalized() def alto_en(T, f): tabla, altos, v = dict(FRAMING), [], FRAMING[0][1] for i in range(len(T.beats)): v = tabla.get(i, v) altos.append(v) i = 0 for k in range(len(T.beats)): if f >= T.span(k)[0]: i = k start, fin = T.span(i) p = min(1.0, (f - start) / max(1.0, (fin - start) * 0.55)) return mix_m(altos[i - 1] if i else altos[0], altos[i], p) def build_scene(T): sc = scene_setup() studio_world(0.65) # what the steel reflects cam = camera_obj((0, -6, 0), (0, 0, 0), lens=50) cam.data.sensor_fit = 'VERTICAL' cam.data.sensor_height = 36.0 light_obj("key", 'AREA', (-3.4, -4.6, 4.4), 1400, "blanco", size_u=6.0, sight=(0, 0, 0)) light_obj("fill", 'AREA', (4.2, -3.2, -1.6), 420, "#BFD4FF", size_u=5.0, sight=(0, 0, 0)) light_obj("rim", 'AREA', (-0.8, 4.6, 2.8), 900, "blanco", size_u=4.0, sight=(0, 0, 0)) M = { "acero": metal("acero", "#B7BEC9", rough=0.22), "acero2": metal("acero2", "#9BA3B0", rough=0.30), "fundido": metal("fundido", "#5E6774", rough=0.62, met=0.85), "eje_i": metal("eje_i", "#C2C8D2", rough=0.24), "eje_d": metal("eje_d", "#C2C8D2", rough=0.24), "goma": material("goma", "#1E222C", rough=0.52), "alu": metal("alu", "#CBD3DE", rough=0.15), "freno": metal("freno", "#79818E", rough=0.45, met=0.9), } # --- top view: the car turning -------------------------------------- floor_obj = bpy.data.objects.new("suelo_g", None) bpy.context.collection.objects.link(floor_obj) arc_list, wheels_a = [], [] for k, (rr, c) in enumerate(((RADIO_GIRO - TRACK_W / 2, "rosa"), (RADIO_GIRO + TRACK_W / 2, "verde"))): pts = [] for i in range(120): th = -0.62 + 1.24 * i / 119.0 pts.append((-RADIO_GIRO + rr * math.cos(th), rr * math.sin(th), 0.0)) cu = curve_poly(f"arco{k}", [pts], thickness_px=0.045, radios=[[0.0] * 120], mat=material(f"m_arco{k}", c, emit=2.4, rough=0.5)) cu.parent = floor_obj arc_list.append((cu, pts)) r = full_wheel(f"ra{k}", M, R=R_WHEEL, r=0.185, width_px=0.28) r.parent = floor_obj wheels_a.append(r) axis_a = cylinder("eje_a", 0.075, TRACK_W, M["acero2"]) axis_a.parent = floor_obj patina = curve_poly("patina", [[(0, 0, 0)] * 60], thickness_px=0.11, radios=[[0.0] * 60], mat=material("m_pat", "blanco", emit=3.4)) patina.parent = floor_obj # --- the differential -------------------------------------------------- diff = bpy.data.objects.new("dif", None) bpy.context.collection.objects.link(diff) diff.scale = (0, 0, 0) cage = bpy.data.objects.new("jaula", None) bpy.context.collection.objects.link(cage) cage.parent = diff # carrier housing: two annular flanges and two curved walls around the pin # axis. The +-Y sides stay open: that is where the gears show, which is how a # real differential housing looks # everything in the carrier stays inside r=0.52: the pinion never goes below # r=0.59, so the carrier can spin without running over it for sx in (0.58, -0.58): br = flat_ring(f"brida{sx}", 0.13, 0.52, 0.08, M["fundido"]) br.rotation_euler = (0, math.pi / 2, 0) br.location = (sx, 0, 0) br.parent = cage for k, center_pt in enumerate((math.pi / 2, -math.pi / 2)): pared = ring_sector(f"pared{k}", 0.43, 0.52, 1.16, center_pt - 0.62, center_pt + 0.62, M["fundido"]) pared.rotation_euler = (0, math.pi / 2, 0) pared.parent = cage bolt = cylinder("perno", 0.062, 1.00, M["acero2"]) bolt.parent = cage corona = bevel_gear("corona", (1, 0, 0), N=N_COR, gamma=G_COR, d_i=D_I, d_o=D_O, alto=0.042, mat=M["acero"], spiral=0.62, thickness=0.14, gap_m=0.58, per_tooth=24) corona.location = (X_COR, 0, 0) corona.parent = cage screws("bulones", 10, 0.78, -0.11, largo=0.10, r_t=0.035, mat=M["acero2"], padre=cage) lat = {} for nm_key, axis_obj, sx in (("izq", (-1, 0, 0), -1), ("der", (1, 0, 0), 1)): g = bpy.data.objects.new(f"lat_{nm_key}", None) bpy.context.collection.objects.link(g) g.parent = diff eng = bevel_gear(f"eng_{nm_key}", axis_obj, N=N_LAT, gamma=45.0, d_i=0.26, d_o=0.50, alto=0.042, mat=M["acero"], spiral=0.30, thickness=0.12, gap_m=0.09) eng.parent = g # stepped half-shaft + flange for (largo, radio, x0) in ((0.48, 0.110, 0.50), (AXIS_X - 0.82, 0.080, 0.95)): se = cylinder(f"se_{nm_key}_{radio}", radio, largo, M[f"eje_{nm_key[0]}"]) se.rotation_euler = (0, math.pi / 2, 0) se.location = (sx * x0, 0, 0) se.parent = g flange = flat_ring(f"bw_{nm_key}", 0.09, 0.22, 0.07, M["acero2"]) flange.rotation_euler = (0, math.pi / 2, 0) flange.location = (sx * (AXIS_X - 0.20), 0, 0) flange.parent = g wheel_obj = full_wheel(f"rue_{nm_key}", M, R=R_WHEEL, r=0.185, width_px=0.28) wheel_obj.location = (sx * AXIS_X, 0, 0) wheel_obj.parent = g lat[nm_key] = g sat = [] for k, axis_obj in enumerate(((0, 0, 1), (0, 0, -1))): g = bpy.data.objects.new(f"sat{k}", None) bpy.context.collection.objects.link(g) g.parent = cage eng = bevel_gear(f"eng_s{k}", axis_obj, N=N_LAT, gamma=45.0, d_i=0.26, d_o=0.50, alto=0.042, mat=M["acero2"], spiral=-0.30, thickness=0.12, gap_m=0.09, phase=math.pi) eng.parent = g if k == 0: p = sphere(f"testigo{k}", 0.050, material("m_test", "ambar", emit=3.0)) p.location = (0.30, 0.0, 0.42) p.parent = g sat.append(g) input_shaft = bpy.data.objects.new("entrada", None) bpy.context.collection.objects.link(input_shaft) input_shaft.parent = diff # same face as the ring gear, opposite-hand spiral and a phase so the # tooth lands in the gap pinion = bevel_gear("pinon", (0, -1, 0), N=N_PIN, gamma=G_PIN, d_i=D_I, d_o=D_O, alto=0.042, mat=M["acero"], spiral=P_ESP, thickness=0.12, gap_m=0.07, phase=P_PHASE, per_tooth=24) pinion.location = (X_COR, 0, 0) pinion.parent = input_shaft cardan_joint = cylinder("cardan", 0.095, 1.30, M["acero2"]) cardan_joint.rotation_euler = (math.pi / 2, 0, 0) cardan_joint.location = (X_COR, -1.50, 0) cardan_joint.parent = input_shaft fork = flat_ring("horquilla", 0.10, 0.24, 0.10, M["acero2"]) fork.location = (X_COR, -1.20, 0) fork.rotation_euler = (math.pi / 2, 0, 0) fork.parent = input_shaft bar_list = {} for nm_key, c, z in (("izq", "rosa", -0.92), ("der", "verde", -0.60)): rail = cylinder(f"riel_{nm_key}", 0.038, 1.50, material(f"mr_{nm_key}", "riel", rough=0.6)) rail.rotation_euler = (0, math.pi / 2, 0) rail.location = (0.90, -0.6, z) b = cylinder(f"bar_{nm_key}", 0.060, 1.0, material(f"mb_{nm_key}", c, emit=2.6)) b.rotation_euler = (0, math.pi / 2, 0) bar_list[nm_key] = (rail, b, z) rail.scale = b.scale = (0, 0, 0) date = txt_m("1827", size_u=0.44, color="ambar") date.scale = (0, 0, 0) def emission(mat, v, col=None): n = mat.node_tree.nodes["Principled BSDF"] n.inputs["Emission Strength"].default_value = v if col is not None: n.inputs["Emission Color"].default_value = (*srgb(col), 1) return dict(cam=cam, M=M, emission=emission, floor_obj=floor_obj, arc_list=arc_list, wheels_a=wheels_a, axis_a=axis_a, patina=patina, diff=diff, lat=lat, cage=cage, sat=sat, input_shaft=input_shaft, pinion=pinion, bar_list=bar_list, date=date) def main(): T = Timeline(NAME_KEY) ob = build_scene(T) M, bar_list = ob["M"], ob["bar_list"] def refresh(f): t = T.t(f) drop_by = suave(T.p(f, 2) / 0.62) dist = 50.0 / 36.0 * alto_en(T, f) aim_at(ob["cam"], Vector((0.0, -1.1, 8.6)).lerp(DIR_CAM * dist, drop_by), Vector((0.0, 0.0, 0.0)).lerp(Vector((0.0, 0.0, -0.50)), drop_by)) # ---- top view visible_g = 1.0 - suave(T.p(f, 2) / 0.45) ob["floor_obj"].scale = (visible_g, visible_g, visible_g) if visible_g > 0.001: av = suave(T.p(f, 0) * 0.62) if f < T.span(1)[0] else \ mix_m(0.62, 1.0, T.p(f, 1)) th = -0.60 + 1.20 * av centers = [] for k, (cu, pts) in enumerate(ob["arc_list"]): rr = RADIO_GIRO + (TRACK_W / 2) * (1 if k else -1) c = Vector((-RADIO_GIRO + rr * math.cos(th), rr * math.sin(th), R_WHEEL)) centers.append(c) n = max(2, int(120 * av)) rebuild_curve(cu, [pts], [[1.0 if i <= n else 0.30 for i in range(120)]]) giro = (rr * (th + 0.60)) / R_WHEEL if f >= T.span(1)[0]: rig = (RADIO_GIRO + TRACK_W / 2) * (th + 0.60) / R_WHEEL giro = mix_m(giro, rig, suave(T.p(f, 1) / 0.35)) r = ob["wheels_a"][k] r.location = c r.rotation_euler = (giro, 0, th) # wheel on its axle, pointing at the centre middle = (centers[0] + centers[1]) / 2 ob["floor_obj"].location = (-middle.x, -middle.y - 0.30, 0.0) ob["axis_a"].location = middle ob["axis_a"].rotation_euler = (0, math.pi / 2, th) pv = suave(T.p(f, 1) / 0.4) rr = RADIO_GIRO - TRACK_W / 2 pts, rad = [], [] for i in range(60): a2 = -0.60 + (th + 0.60) * i / 59.0 pts.append((-RADIO_GIRO + rr * math.cos(a2), rr * math.sin(a2), 0.04)) rad.append(pv * min(1.0, (i / 59.0) * 2.4)) rebuild_curve(ob["patina"], [pts], [rad]) # ---- the differential ap = suave((T.p(f, 2) - 0.35) / 0.5) ob["diff"].scale = (ap, ap, ap) ob["diff"].rotation_euler = (0, INCL, 0) starts_at = T.span(6)[0] tt = max(0.0, t - T.t(starts_at)) k_giro = mix_m(0.0, 0.42, T.p(f, 8) / 0.7) ang_j = OMEGA * tt if f >= starts_at else 0.0 ang_i = ang_j * (1.0 - k_giro) ang_d = ang_j * (1.0 + k_giro) ob["cage"].rotation_euler = (ang_j, 0, 0) ob["lat"]["izq"].rotation_euler = (ang_i, 0, 0) ob["lat"]["der"].rotation_euler = (ang_d, 0, 0) for k, g in enumerate(ob["sat"]): g.rotation_euler = (0, 0, (ang_i - ang_d) / 2 * (1 if k == 0 else -1)) ob["pinion"].rotation_euler = (0, P_NEXT * ang_j * N_COR / N_PIN, 0) # ---- highlights in the channel colors, over the steel def peak_y(i): p = T.p(f, i) return 0.85 * math.sin(math.pi * p) ** 2 if 0.0 < p < 1.0 else 0.0 ob["emission"](M["eje_i"], peak_y(4), "rosa") ob["emission"](M["eje_d"], peak_y(4), "verde") ob["emission"](M["acero2"], peak_y(5), "celeste") ob["emission"](M["fundido"], peak_y(5), "ambar") ob["emission"](M["acero"], peak_y(6), "ambar") # ---- speed bars bv = suave(T.p(f, 9) / 0.4) * (1.0 - suave(T.p(f, 10) / 0.3)) for nm_key, (rail, bar, z) in bar_list.items(): rail.scale = (bv, bv, bv) rel = (1.0 - k_giro) if nm_key == "izq" else (1.0 + k_giro) L = 1.50 * rel / 1.5 bar.scale = (bv, bv, bv * L) bar.location = (0.15 + L / 2, -0.6, z) fv = suave((T.p(f, 10) - 0.25) / 0.4) ob["date"].location = (0.90, 0.0, -0.80) ob["date"].scale = (fv, fv, fv) render_sequence(NAME_KEY, T, refresh) main()