
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
드레드넛은 스틸 줄을 쓰는 어쿠스틱 기타에서 가장 널리 쓰이는 몸통형이다. 미국의 C. F. 마틴 사가 1916년 처음 만들었고, 이름은 당시 영국 해군의 대형 전함 드레드넛에서 따왔다. 당시 기타보다 몸통이 크고 깊어 저음이 풍부하고 음량이 커서, 컨트리·포크·블루그래스 연주자들 사이에서 표준처럼 자리 잡았다.
허리가 비교적 얕게 들어가고 위 볼의 어깨가 각진 것이 드레드넛을 알아보게 하는 특징이다. 가문비나무 앞판에는 둥근 사운드홀과 상감 로제트가 있고, 그 아래로 물방울 모양 픽가드와 브리지가 놓인다. 넥은 14번 프렛에서 몸통과 만나며, 뒤로 꺾인 헤드에 줄감개가 양쪽에 셋씩 달린다.
이 모델은 로즈우드 옆·뒤판과 상아색 바인딩, 흑단 지판을 갖춘 전형적인 드레드넛을 접이식 스탠드에 기대 세운 모습으로 표현했다. 줄 길이 645mm를 기준으로 프렛과 브리지 위치를 계산해, 실제 악기와 같은 비례로 지판과 몸통이 이어진다.
만든 방법
몸통 윤곽은 오른쪽 절반의 기준점 17개를 캐트멀-롬 곡선으로 잇고 좌우 대칭으로 닫은 뒤 호 길이로 192점 재표본했다. 이 윤곽을 안쪽 법선 방향으로 조금씩 들여 8층으로 쌓아 둥근 모서리를 만들고, 층 사이 면마다 앞판·바인딩·옆판 재질 번호를 나눠 한 메시에 담았다. 사운드홀은 EXACT 불리언으로 뚫어 속면에 어두운 재질을 옮겼고, 로제트와 퍼플링은 앞판을 살짝 관통하는 얇은 고리로 넣어 면 겹침을 피했다. 넥은 반타원 단면 61개를 이어 힐과 헤드 쪽 전이까지 한 번에 만들었고, 프렛 20개는 하나의 메시를 폭만 다르게 늘려 공유했다. 줄은 브리지 핀-새들-너트-줄감개 기둥을 잇는 관이며, 나뭇결·줄의 감긴 질감·상표 로고는 생략했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 404줄.
# 드레드넛 통기타(스틸 스트링 어쿠스틱 기타)와 접이식 기타 스탠드.# 몸통 윤곽은 손으로 찍은 기준점을 캐트멀-롬 곡선으로 이어 만들고, 그 윤곽을 층층이 쌓아(로프트)# 앞판·바인딩·옆판·뒤판을 하나의 메시에 면별 재질로 나눴다. 사운드홀은 불리언으로 뚫었다.# 넥은 반타원 단면을 길이 방향으로 이어 만들었고, 프렛 위치는 평균율 공식 S·(1-2^(-n/12))로 계산했다.# 헤드는 14° 뒤로 꺾인 판에 3+3 줄감개를 달았고, 여섯 줄은 브리지 핀-새들-너트-줄감개 기둥을 잇는 관이다.# 기타는 로컬 좌표(앞판 +Z, 넥 +Y)에서 만든 뒤 빈 오브젝트에 묶어 75° 세워 스탠드에 기대 놓았다.from atelier import *SHOT = {"azimuth": -62, "elevation": 10, "lens": 60, "light": "studio", "background": "#25323b"}# ---------------------------------------------------------------- 재질M_TOP = mat('Spruce_Top', '#d8b27a', 0.38, coat=0.6)M_BACK = mat('Rosewood_BackSides', '#4b2617', 0.32, coat=0.6)M_NECK = mat('Mahogany_Neck', '#6e3a22', 0.4, coat=0.4)M_EBONY = mat('Ebony_Fretboard', '#251a15', 0.45)M_BIND = mat('Ivoroid_Binding', '#e6dbbf', 0.35)M_HOLE = mat('Body_Interior', '#2e1c11', 0.8)M_GUARD = mat('Tortoise_Pickguard', '#4a1a0c', 0.42)M_FRET = mat('Nickel_Fretwire', '#cfc9bb', 0.22, 1.0)M_CHROME = mat('Chrome_Tuners', '#d6d8da', 0.12, 1.0)M_BRONZE = mat('Bronze_Strings', '#c09a5c', 0.3, 1.0)M_STEEL = mat('Steel_Strings', '#c6c7c8', 0.2, 1.0)M_PEARL = mat('Pearl_Inlay', '#ece6d8', 0.2)M_STAND = mat('Stand_PowderCoat', '#2c2d30', 0.45, 0.6)M_RUBBER = mat('Stand_Rubber', '#3a2f2b', 0.85)# ---------------------------------------------------------------- 치수(미터)BODY_L = 0.505 # 몸통 길이SCALE = 0.645 # 줄 길이(너트~새들)fret_d = lambda n: SCALE * (1 - 2 ** (-n / 12.0))Y_NUT = BODY_L + fret_d(14) # 14번 프렛이 몸통 끝과 만난다Y_SADDLE = Y_NUT - SCALEHOLE_C = (0.0, 0.362)HOLE_R = 0.0505FB_TOP = 0.006 # 지판 윗면 높이HEAD_ANG = math.radians(14)def depth_at(y): """몸통 두께: 꼬리 쪽 12.2cm → 넥 쪽 10cm.""" return 0.122 - 0.022 * min(max(y / BODY_L, 0), 1)# ---------------------------------------------------------------- 공용 도구def catmull(pts, n_per=24): """닫힌 캐트멀-롬 곡선 샘플링.""" out = [] N = len(pts)나머지 356줄 펼치기접기
for i in range(N): p0, p1, p2, p3 = (Vector(pts[(i + k) % N]) for k in (-1, 0, 1, 2)) for s in range(n_per): t = s / n_per t2, t3 = t * t, t * t * t out.append(0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3)) return outdef resample(pts, n): """닫힌 다각형을 호 길이로 균등 재표본.""" seg = [(pts[i] - pts[(i + 1) % len(pts)]).length for i in range(len(pts))] total = sum(seg) out, acc, i = [], 0.0, 0 for k in range(n): target = total * k / n while acc + seg[i] < target: acc += seg[i] i += 1 t = (target - acc) / seg[i] out.append(pts[i].lerp(pts[(i + 1) % len(pts)], t)) return outdef loft(name, rings, mats, ring_mats, cap_start=None, cap_end=None, closed=True, smooth_all=True): """같은 점 개수의 고리들을 차례로 잇는다. ring_mats[i] = i번째와 i+1번째 고리 사이 면의 재질 번호.""" bm = bmesh.new() vr = [[bm.verts.new(p) for p in r] for r in rings] n = len(rings[0]) for i in range(len(rings) - 1): a, b = vr[i], vr[i + 1] for j in range(n if closed else n - 1): j2 = (j + 1) % n f = bm.faces.new((a[j], a[j2], b[j2], b[j])) f.material_index = ring_mats[i] f.smooth = smooth_all if cap_start is not None: f = bm.faces.new(list(reversed(vr[0]))) f.material_index = cap_start if cap_end is not None: f = bm.faces.new(vr[-1]) f.material_index = cap_end bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) link(ob) for m in mats: me.materials.append(m) return obdef annulus(name, r1, r2, z0, z1, material, seg=96): return lathe(name, [(r1, z0), (r2, z0), (r2, z1), (r1, z1), (r1, z0)], material=material, seg=seg, shade_smooth=False)def tube(name, points, radius, material, bevel_res=3): """curve_tube 를 만든 뒤 바로 메시로 바꾼다(경계상자·내보내기 안정).""" cu = curve_tube(name, points, radius=radius, material=material, resolution=1, bevel_res=bevel_res) dg = bpy.context.evaluated_depsgraph_get() me = bpy.data.meshes.new_from_object(cu.evaluated_get(dg)) bpy.data.objects.remove(cu, do_unlink=True) ob = bpy.data.objects.new(name, me) link(ob) return smooth(ob)GUITAR_PARTS = []def G(ob): GUITAR_PARTS.append(ob) return ob# ================================================================ 몸통# 오른쪽 절반 기준점(x=반폭, y=꼬리에서의 거리). 아래 볼 40cm, 허리 27cm, 위 볼 29cm.half = [(0.0, 0.0), (0.075, 0.004), (0.135, 0.022), (0.176, 0.052), (0.197, 0.098), (0.199, 0.145), (0.188, 0.195), (0.165, 0.243), (0.142, 0.285), (0.137, 0.312), (0.141, 0.345), (0.146, 0.385), (0.146, 0.425), (0.139, 0.462), (0.118, 0.488), (0.075, 0.503), (0.0, BODY_L)]key = half + [(-x, y) for x, y in reversed(half[1:-1])]outline = resample(catmull([Vector((x, y, 0)) for x, y in key], 20), 192)N = len(outline)norms = []for i in range(N): t = (outline[(i + 1) % N] - outline[i - 1]).normalized() norms.append(Vector((-t.y, t.x, 0))) # 반시계 윤곽의 안쪽 법선def ring_at(zfn, inset): return [Vector((p.x + nrm.x * inset, p.y + nrm.y * inset, zfn(p.y))) for p, nrm in zip(outline, norms)]R = 0.0024layers = [ (lambda y: 0.0, R), (lambda y: -0.0007, 0.0007), (lambda y: -R, 0.0), (lambda y: -0.0065, 0.0), (lambda y: -depth_at(y) + 0.0065, 0.0), (lambda y: -depth_at(y) + R, 0.0), (lambda y: -depth_at(y) + 0.0007, 0.0007), (lambda y: -depth_at(y), R),]rings = [ring_at(z, ins) for z, ins in layers]# 재질 번호: 0 앞판, 1 바인딩, 2 옆·뒤판, 3 속(사운드홀)body = loft('Guitar_Body', rings, [M_TOP, M_BIND, M_BACK, M_HOLE], [1, 1, 1, 2, 1, 1, 1], cap_start=0, cap_end=2)for p in body.data.polygons: if p.material_index in (0, 2) and abs(p.normal.z) > 0.9: p.use_smooth = Falsehole_cut = cylinder('SoundHole_Cutter', HOLE_R, 0.105, loc=(HOLE_C[0], HOLE_C[1], -0.095), material=M_HOLE, seg=64)boolean(body, hole_cut)body.modifiers[-1].material_mode = 'TRANSFER'apply_all(body)G(body)# 로제트(사운드홀 둘레 상감 고리)G(annulus('Rosette_Band', 0.0565, 0.0635, -0.0004, 0.0002, M_BACK))G(annulus('Rosette_Ring_Inner', 0.0555, 0.0575, -0.0004, 0.0004, M_BIND))G(annulus('Rosette_Ring_Outer', 0.0625, 0.0645, -0.0004, 0.0004, M_BIND))G(annulus('Rosette_Ring_Outer2', 0.0660, 0.0668, -0.0004, 0.0003, M_EBONY))G(annulus('SoundHole_Edge', HOLE_R - 0.0001, HOLE_R + 0.0012, -0.0035, 0.0001, M_TOP))for ob in GUITAR_PARTS[-5:]: ob.location = (HOLE_C[0], HOLE_C[1], 0)# 앞판 가장자리 퍼플링(검은 줄)pur = loft('Top_Purfling', [ring_at(lambda y: -0.0004, 0.0042), ring_at(lambda y: 0.00025, 0.0042), ring_at(lambda y: 0.00025, 0.0058), ring_at(lambda y: -0.0004, 0.0058)], [M_EBONY], [0, 0, 0], smooth_all=False)G(pur)# 뒤판 가운데 띠(백 스트립)_sl = math.atan(0.022 / BODY_L)G(box('Back_Center_Strip', (0.0055, 0.484, 0.0012), loc=(0, 0.2535, -depth_at(0.2535)), rot=(_sl, 0, 0), material=M_BIND))# 픽가드(물방울형, 사운드홀 오른쪽 아래를 감싼다)pg = []A0, A1 = math.radians(-118), math.radians(28)K = 40for k in range(K + 1): a = A0 + (A1 - A0) * k / K pg.append((HOLE_C[0] + 0.0688 * math.cos(a), HOLE_C[1] + 0.0688 * math.sin(a)))for k in range(K, -1, -1): u = k / K a = A0 + (A1 - A0) * u w = 0.064 * math.sin(math.pi * (u ** 0.85)) ** 0.55 * (1 - 0.35 * u) r = 0.0688 + w pg.append((HOLE_C[0] + r * math.cos(a), HOLE_C[1] + r * math.sin(a)))G(prism('Pickguard', pg[:-1], 0.0007, loc=(0, 0, -0.0003), material=M_GUARD))# 브리지(배불뚝이형)와 새들, 브리지 핀bp = []BL, BW = 0.076, 0.0155for k in range(25): t = -1 + 2 * k / 24 bp.append((BL * t, -BW - 0.004 * math.cos(t * math.pi / 2) ** 2))for k in range(25): t = 1 - 2 * k / 24 bp.append((BL * t * 0.985, BW))bridge = prism('Bridge', bp, 0.0095, loc=(0, Y_SADDLE - 0.006, -0.0005), material=M_EBONY)bevel(bridge, 0.0025, 3, 40)G(bridge)saddle = box('Saddle', (0.074, 0.0028, 0.0055), loc=(0, Y_SADDLE, 0.0074), rot=(0, 0, math.radians(1.5)), material=M_BIND, base=True)bevel(saddle, 0.0009, 2, 40)G(saddle)Y_PIN = Y_SADDLE - 0.0135pin = join([cylinder('Pin_shaft', 0.0026, 0.004, loc=(0, 0, 0.0075), material=M_BIND, seg=16), sphere('Pin_head', 0.0034, loc=(0, 0, 0.0108), material=M_BIND, seg=16, rings=8, scale=(1, 1, 0.55))], 'Bridge_Pin_1')G(pin)STR_X_SADDLE = [-0.0262 + 0.0524 * i / 5 for i in range(6)]STR_X_NUT = [-0.0178 + 0.0356 * i / 5 for i in range(6)]for i, x in enumerate(STR_X_SADDLE): if i == 0: pin.location = (x, Y_PIN, 0.0075) else: G(duplicate(pin, loc=(x, Y_PIN, 0.0075), name=f'Bridge_Pin_{i + 1}'))# 꼬리 스트랩 버튼G(join([cylinder('EndPin_base', 0.0055, 0.004, rot=(math.pi / 2, 0, 0), loc=(0, 0.0015, -0.06), material=M_BIND, seg=20), cylinder('EndPin_knob', 0.0045, 0.008, r2=0.0035, rot=(math.pi / 2, 0, 0), loc=(0, -0.002, -0.06), material=M_BIND, seg=20)], 'End_Pin'))# ================================================================ 넥FB_END = 0.4175def fb_half(y): return 0.5 * (0.0435 + (Y_NUT - y) * (0.0125 / fret_d(14)))def neck_depth(y): base = 0.0212 + 0.0028 * min(max((Y_NUT - y) / (Y_NUT - 0.57), 0), 1) t = min(max((0.562 - y) / (0.562 - 0.506), 0), 1) heel = t * t * (3 - 2 * t) return base + (depth_at(0.5) - 0.002 - base) * heelnrings = []ys = [0.497 + (Y_NUT + 0.028 - 0.497) * k / 60 for k in range(61)]for y in ys: hw = fb_half(min(y, Y_NUT)) d = neck_depth(min(y, Y_NUT)) ztop = 0.0 if y > Y_NUT: u = (y - Y_NUT) / 0.028 ztop = -(y - Y_NUT) * math.tan(HEAD_ANG) - 0.0016 * min(u * 4, 1) d = d + (0.0152 - d) * u if y < 0.575: hw *= 0.93 + 0.07 * (y - 0.497) / 0.078 ring = [] for j in range(19): a = math.pi * j / 18 s = math.sin(a) ring.append(Vector((hw * math.cos(a), y, ztop - d * s ** 0.85))) nrings.append(ring)neck = loft('Neck', nrings, [M_NECK], [0] * 60, cap_start=0, cap_end=0, closed=True)G(neck)# 지판fbp = [(-fb_half(FB_END), FB_END), (fb_half(FB_END), FB_END), (fb_half(Y_NUT), Y_NUT), (-fb_half(Y_NUT), Y_NUT)]G(prism('Fretboard', fbp, FB_TOP + 0.0005, loc=(0, 0, -0.0005), material=M_EBONY))# 프렛 20개(같은 메시를 폭만 늘려 공유)fret = cylinder('Fret_01', 0.0011, 1.0, rot=(0, math.pi / 2, 0), loc=(-0.5, 0, 0), material=M_FRET, seg=10, base=True)G(fret)for n in range(1, 21): y = Y_NUT - fret_d(n) w = 2 * fb_half(y) - 0.0004 ob = fret if n == 1 else G(duplicate(fret, name=f'Fret_{n:02d}')) ob.location = (-w / 2, y, FB_TOP - 0.0002) ob.scale = (1, 1, w)# 포지션 마크(자개 점)dot = cylinder('Inlay_Dot', 0.0032, 0.0008, loc=(0, 0, FB_TOP - 0.0007), material=M_PEARL, seg=20)G(dot)first = Truefor n in (3, 5, 7, 9, 12, 15, 17): y = Y_NUT - 0.5 * (fret_d(n) + fret_d(n - 1)) for x in ((-0.0075, 0.0075) if n == 12 else (0.0,)): if first: dot.location = (x, y, FB_TOP - 0.0007) first = False else: G(duplicate(dot, loc=(x, y, FB_TOP - 0.0007), name=f'Inlay_Dot_{n}'))# 너트nut = box('Nut', (2 * fb_half(Y_NUT), 0.005, 0.0088), loc=(0, Y_NUT + 0.0025, -0.0005), material=M_BIND, base=True)bevel(nut, 0.0008, 2, 40)G(nut)# ================================================================ 헤드HEAD_L = 0.19head_m = Matrix.Translation((0, Y_NUT, 0)) @ Matrix.Rotation(-HEAD_ANG, 4, 'X')hp = [(-0.0375, 0.0), (0.0375, 0.0), (0.0435, HEAD_L - 0.012), (0.04, HEAD_L), (-0.04, HEAD_L), (-0.0435, HEAD_L - 0.012)]bmh = bmesh.new()bot = [bmh.verts.new((x, y, -0.0155)) for x, y in hp]topv = [bmh.verts.new((x, y, 0.0)) for x, y in hp]bmh.faces.new(list(reversed(bot))).material_index = 0bmh.faces.new(topv).material_index = 1for i in range(len(hp)): bmh.faces.new((bot[i], bot[(i + 1) % len(hp)], topv[(i + 1) % len(hp)], topv[i])).material_index = 0bmesh.ops.recalc_face_normals(bmh, faces=bmh.faces)me = bpy.data.meshes.new('Headstock')bmh.to_mesh(me)bmh.free()me.materials.append(M_NECK)me.materials.append(M_EBONY)headstock = bpy.data.objects.new('Headstock', me)link(headstock)headstock.matrix_world = head_mbevel(headstock, 0.0018, 2, 40)G(headstock)# 줄감개 3+3 (기둥은 앞면, 기어 상자와 손잡이는 뒷면 바깥쪽)POST_Y = [0.045, 0.087, 0.129]POST_X = 0.0255tuner_parts = [ cylinder('Tuner_bushing', 0.0048, 0.0022, loc=(0, 0, -0.0004), material=M_CHROME, seg=20), cylinder('Tuner_post', 0.003, 0.0145, loc=(0, 0, 0), material=M_CHROME, seg=16), box('Tuner_gearbox', (0.017, 0.016, 0.0105), loc=(0.004, 0, -0.0155 - 0.0105 + 0.0004), material=M_CHROME, base=True), cylinder('Tuner_shaft', 0.0018, 0.022, rot=(0, math.pi / 2, 0), loc=(0.010, 0, -0.0205), material=M_CHROME, seg=12), sphere('Tuner_button', 0.0105, loc=(0.035, 0, -0.0205), material=M_CHROME, seg=24, rings=12, scale=(0.72, 0.28, 0.95)),]bevel(tuner_parts[2], 0.002, 2, 40)tuner = join(tuner_parts, 'Tuner_Treble_1')smooth(tuner)POSTS_WORLD = []k = 0for side in (-1, 1): # -1 = 저음 쪽(관람자 왼쪽) for j, py in enumerate(POST_Y): ob = tuner if k == 0 else duplicate(tuner) ob.name = f"Tuner_{'Bass' if side < 0 else 'Treble'}_{j + 1}" ob.matrix_world = head_m @ Matrix.Translation((side * POST_X, py, 0)) @ Matrix.Diagonal((side, 1, 1, 1)) G(ob) POSTS_WORLD.append(head_m @ Vector((side * POST_X, py, 0.0105))) k += 1# ================================================================ 줄 6개# 줄 순서: 0=6번(저음 E) … 5=1번(고음 E). 저음 3줄은 왼쪽 기둥, 고음 3줄은 오른쪽 기둥.post_for = [POSTS_WORLD[2], POSTS_WORLD[1], POSTS_WORLD[0], POSTS_WORLD[3], POSTS_WORLD[4], POSTS_WORLD[5]]radii = [0.00068, 0.00058, 0.00048, 0.00040, 0.00030, 0.00025]for i in range(6): xs, xn = STR_X_SADDLE[i], STR_X_NUT[i] post = post_for[i] pts = [(xs, Y_PIN, 0.0100), (xs, Y_SADDLE - 0.0006, FB_TOP + 0.0065 + 0.0000), (xs, Y_SADDLE + 0.0006, FB_TOP + 0.0068), (xn, Y_NUT + 0.0012, FB_TOP + 0.0029), (xn, Y_NUT + 0.0045, FB_TOP + 0.0022), (post.x - 0.0028 * (1 if post.x > 0 else -1) * 0, post.y - 0.0, post.z)] st = tube(f'String_{6 - i}', pts, radii[i], M_BRONZE if i < 4 else M_STEEL, bevel_res=1) G(st)# ================================================================ 세우기pivot = bpy.data.objects.new('Guitar_Dreadnought', None)link(pivot)TILT = math.radians(75)pivot.matrix_world = Matrix.Translation((0, 0, 0.075)) @ Matrix.Rotation(TILT, 4, 'X')GM = pivot.matrix_world.copy()for ob in GUITAR_PARTS: ob.parent = pivotdef W(x, y, z): return tuple(GM @ Vector((x, y, z)))# ================================================================ 스탠드TR = 0.0085HUB = Vector((0, 0.34, 0.13))# 받침 팔: 몸통 꼬리 쪽 아래 면을 따라가다 앞에서 위로 꺾인다for side in (-1, 1): x = side * 0.105 # 이 x 에서 몸통 윤곽의 꼬리쪽 y yb = min(p.y for p in outline if abs(p.x - x) < 0.006 and p.y < 0.1) off = TR + 0.0035 front = Vector(W(x, yb - off, 0.012)) under_a = Vector(W(x, yb - off, -0.004)) under_b = Vector(W(x, yb - off, -depth_at(yb) + 0.006)) back = Vector(W(x, yb - off - 0.01, -depth_at(yb) - 0.02)) pad = tube(f'Stand_Cradle_Pad_{"L" if side < 0 else "R"}', [front + Vector((0, 0, 0.03)), front, under_a, under_b], TR + 0.002, M_RUBBER) arm = tube(f'Stand_Cradle_Arm_{"L" if side < 0 else "R"}', [under_b + (under_b - under_a).normalized() * -0.002, back, HUB + Vector((side * 0.01, 0, 0))], TR, M_STAND)# 목 받침(U자 요크) - 기타 좌표에서 만든다YOKE_Y = 0.80uk = []for k in range(17): a = math.pi * k / 16 uk.append(W(0.031 * math.cos(a), YOKE_Y, 0.004 - 0.0305 * math.sin(a)))yoke = tube('Stand_Neck_Yoke', uk, 0.0055, M_RUBBER)yoke_bottom = Vector(W(0, YOKE_Y, 0.004 - 0.0305 - 0.004))tube('Stand_Mast', [yoke_bottom, yoke_bottom + Vector((0, 0.012, -0.03)), HUB], TR, M_STAND)# 다리 셋과 고무발feet = [Vector((-0.2, 0.06, 0.0)), Vector((0.2, 0.06, 0.0)), Vector((0, 0.58, 0.0))]for i, f in enumerate(feet): tube(f'Stand_Leg_{i + 1}', [HUB, f + Vector((0, 0, TR + 0.006))], TR, M_STAND) sphere(f'Stand_Foot_{i + 1}', 0.016, loc=f + Vector((0, 0, 0.012)), material=M_RUBBER, seg=20, rings=10, scale=(1, 1, 0.75))sphere('Stand_Hub', 0.02, loc=HUB, material=M_STAND, seg=24, rings=12)






