
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
새턴 V는 미국 NASA가 아폴로 계획을 위해 개발한 3단식 대형 로켓이다. 1967년 첫 발사부터 1973년 스카이랩 발사까지 모두 13회 발사되었고, 1969년 아폴로 11호를 포함해 유인 달 탐사선을 실어 보냈다. 높이는 약 110.6m, 1단 지름은 약 10.1m이다.
1단(S-IC)은 F-1 엔진 5기로 이륙 추력을 냈고, 2단(S-II)은 J-2 엔진 5기, 3단(S-IVB)은 J-2 엔진 1기를 썼다. 3단 위에는 비행을 제어하는 계기부(IU), 달착륙선을 감싼 원뿔형 어댑터(SLA), 기계선과 사령선이 차례로 올라가고, 맨 위에는 비상시 사령선을 끌어내는 발사 탈출 탑이 섰다.
흰 동체 위의 검은 띠와 체크무늬는 비행 중 로켓의 회전을 카메라로 판별하기 위한 도장이다. 이 모델은 1단의 세로 띠와 탱크 사이 구간의 흑백 칸, 3단 후방의 사분면 무늬, 성조기와 USA 표기, 엔진 페어링과 날개 4쌍, F-1 엔진의 노즐 연장부와 터빈 배기 매니폴드를 표현했다. 로켓은 발사 때처럼 이동식 발사대 갑판의 배기 구멍 위에 고정 팔로 얹혀 있다.
만든 방법
동체 전체는 반지름·높이 프로필 한 줄을 Z 축으로 돌린 회전체 하나이며, 둘레를 96칸으로 나눈 면마다 재질 번호를 매겨 흑백 롤 패턴을 겹친 면 없이 칠했다. F-1 엔진은 노즐 연장부와 연소실을 두 회전체로 나누고 터보펌프·배관을 합쳐 한 부품으로 만든 뒤 메시를 공유해 5기를 배치했다. 탱크 사이 구간과 스커트의 코루게이션 보강재는 같은 상자를 둘레에 반복 배치하고 바탕 도장 색을 따라가게 했다. 성조기와 글자는 평면 메시를 만든 뒤 정점을 원통 좌표로 옮겨 동체 곡면에 감았다. 발사 탈출 탑은 기울어진 원기둥 부재로 짠 트러스이다. 이동식 발사대는 가운데 정사각 배기 구멍을 뚫은 갑판 하나와 고정 팔 4개, 꼬리 서비스 마스트 3기만 두었고, 발사 엄빌리컬 탑은 넣지 않았다. 실제 기체의 리벳, 소형 안테나, 역추진 모터 덮개, 세부 배관과 단마다 다른 표기 문구는 생략했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 392줄.
# 새턴 V 로켓 (아폴로 계획 발사 형상) — Blender Python 코드만으로 만든 실물 크기 모델(전체 높이 약 110.6m).# 동체는 단면 프로필(반지름, 높이)을 Z 축으로 돌린 회전체 한 개로 만들고, 면마다 재질 번호를 매겨# 1단(S-IC)·3단(S-IVB)의 흑백 '롤 패턴' 도장을 겹친 면 없이 칠했다.# F-1 엔진 5기는 노즐 벨 회전체 + 터보펌프·배기 매니폴드를 하나로 묶은 뒤 메시를 공유해 배치했고,# 엔진 페어링·날개 4쌍, 단 사이 이음 링, 얼리지 모터 덮개, APS 모듈, 사령·기계선, 발사 탈출 탑(트러스)을 더했다.# 성조기와 'USA' 글자는 평면 메시를 동체 곡면에 감아(원통 좌표 변환) 붙였다.# 로켓은 이동식 발사대(Mobile Launcher) 갑판의 배기 구멍 위에 고정 팔 4개로 얹혀 있고, 갑판에는 꼬리 서비스 마스트 3기를 세웠다.from atelier import *SHOT = {"azimuth": -50, "elevation": 7, "lens": 70, "light": "day", "background": "#2a3340"}SEG = 96 # 동체 둘레 분할 (45° = 12칸)WHITE = mat('Paint_White', '#e6e4dc', 0.45)BLACK = mat('Paint_Black', '#1f2124', 0.5)IU = mat('Instrument_Unit_Grey', '#c9c8c1', 0.5)SLA = mat('SLA_Panel_Silver', '#d2d3cf', 0.4, 0.35)SILVER = mat('ServiceModule_Aluminium', '#b9bcbf', 0.3, 0.9)HEAT = mat('HeatShield_Dark', '#3b3a38', 0.8)NOZZLE = mat('F1_Nozzle_Extension_Black', '#2c2b2a', 0.55, 0.5)PUMP = mat('F1_Engine_Steel', '#7d7a73', 0.4, 0.85)LES_RED = mat('LES_Tower_Orange', '#b2402c', 0.5)RED = mat('Flag_Red', '#a8262f', 0.5)BLUE = mat('Flag_Blue', '#2a3570', 0.5)ML_STEEL = mat('Launcher_Steel_Grey', '#6e7275', 0.7, 0.3)R1 = 5.03 # S-IC / S-II 반지름 (지름 10.06m)R3 = 3.30 # S-IVB 반지름 (지름 6.6m)RSM = 1.96 # 사령·기계선 반지름 (지름 3.9m)# ---------------------------------------------------------------- 동체 회전체(면별 도장)def painted_lathe(name, profile, seg, mats, paint, cap_mat=None): """profile=[(r,z),...] 아래→위. 구간마다 정점을 따로 만들어 단 경계가 각지게 남도록 한다. paint(theta_deg, z_mid) → mats 인덱스.""" bm = bmesh.new() for j in range(len(profile) - 1): (ra, za), (rb, zb) = profile[j], profile[j + 1] zm = (za + zb) / 2 ringa, ringb = [], [] for i in range(seg): a = 2 * math.pi * i / seg ringa.append(bm.verts.new((ra * math.cos(a), ra * math.sin(a), za)) if ra > 0 else None) ringb.append(bm.verts.new((rb * math.cos(a), rb * math.sin(a), zb)) if rb > 0 else None) ca = bm.verts.new((0, 0, za)) if ra == 0 else None cb = bm.verts.new((0, 0, zb)) if rb == 0 else None for i in range(seg): k = (i + 1) % seg나머지 344줄 펼치기접기
if ra == 0: f = bm.faces.new((ca, ringb[i], ringb[k])) elif rb == 0: f = bm.faces.new((ringa[i], ringa[k], cb)) else: f = bm.faces.new((ringa[i], ringa[k], ringb[k], ringb[i])) th = math.degrees(2 * math.pi * (i + 0.5) / seg) f.material_index = paint(th, zm) f.smooth = abs(rb - ra) < 1e-6 or (ra > 0 and rb > 0) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() for m in mats: me.materials.append(m) ob = bpy.data.objects.new(name, me) link(ob) return obZ_BASE = 5.0HULL = [ (0, Z_BASE), (R1, Z_BASE), # 1단 바닥 열차폐판 (R1, 22.0), (R1, 26.5), (R1, 39.0), (R1, 42.0), # S-IC: 연료탱크 / 탱크 사이 / 산화제탱크 / 전방 스커트 (R1, 47.6), # S-IC/S-II 단간부 (R1, 66.4), # S-II (R3, 72.0), # S-II/S-IVB 원뿔 단간부 (R3, 75.0), (R3, 85.0), # S-IVB (R3, 85.9), # 계기부(IU) (RSM, 94.4), # 달착륙선 어댑터(SLA) (RSM, 98.2), # 기계선(SM) (0.42, 101.3), (0.30, 101.45), (0, 101.45), # 사령선 + 보호 덮개(BPC)]I_W, I_B, I_IU, I_SLA, I_SM, I_HEAT = range(6)def hull_paint(th, z): if z <= Z_BASE + 1e-3: return I_HEAT if z < 22.0: # 연료탱크: 네 줄의 검은 세로 띠 d = min(abs(((th - q) + 180) % 360 - 180) for q in (0, 90, 180, 270)) return I_B if d < 15 else I_W if z < 26.5: # 탱크 사이 구간: 8칸 체크 return I_B if int(th // 45) % 2 == 0 else I_W if z < 39.0: return I_W if z < 42.0: # 전방 스커트: 엇갈린 8칸 return I_B if int(th // 45) % 2 == 1 else I_W if z < 72.0: return I_W if z < 75.0: # S-IVB 후방: 사분면 흑백 return I_B if int(th // 90) % 2 == 0 else I_W if z < 85.0: return I_W if z < 85.9: return I_IU if z < 94.4: return I_SLA if z < 98.2: return I_SM return I_Whull = painted_lathe('SaturnV_Hull', HULL, SEG, [WHITE, BLACK, IU, SLA, SILVER, HEAT], hull_paint)# ---------------------------------------------------------------- 단 이음 링def seam(name, r, z, t=0.07, material=WHITE): return torus(name, R=r, r=t, loc=(0, 0, z), material=material, seg=SEG, rseg=8)for nm, r, z in [('Seam_SIC_Aft', R1, 5.25), ('Seam_SIC_Intertank_Low', R1, 22.0), ('Seam_SIC_Intertank_High', R1, 26.5), ('Seam_SIC_Forward', R1, 39.0), ('Seam_SIC_Top', R1, 42.0), ('Seam_SII_Bottom', R1, 47.6), ('Seam_SII_Top', R1, 66.4), ('Seam_SIVB_Bottom', R3, 72.0), ('Seam_SIVB_Top', R3, 85.0), ('Seam_IU_Top', R3, 85.9), ('Seam_SLA_Top', RSM, 94.4), ('Seam_SM_Top', RSM, 98.2)]: seam(nm, r + 0.02, z, 0.08, IU)# 탱크 사이 구간·전방 스커트·S-IVB 후방 스커트의 세로 보강재(코루게이션) — 바탕 도장 색을 따른다def stringers(prefix, r, z0, z1, every=1): pieces = {} for i in range(0, SEG, every): th = 360 * (i + 0.5) / SEG idx = hull_paint(th, (z0 + z1) / 2) m = (WHITE, BLACK)[idx == I_B] if m.name not in pieces: pieces[m.name] = box(f'{prefix}_Stringer', size=(0.10, 2 * math.pi * r / SEG * 0.42, z1 - z0), material=m, base=True) ob = pieces[m.name] else: ob = duplicate(pieces[m.name]) a = math.radians(th) ob.location = (r * math.cos(a), r * math.sin(a), z0) ob.rotation_euler.z = astringers('Intertank', R1, 22.15, 26.35)stringers('SIC_ForwardSkirt', R1, 39.15, 41.85)stringers('SIVB_AftSkirt', R3, 72.15, 74.85)# 시스템 터널(케이블 통로): 1단·2단 옆을 따라 내려가는 돌출 덮개for nm, z0, z1 in [('SystemsTunnel_SIC', 5.6, 41.6), ('SystemsTunnel_SII', 48.0, 66.0)]: cylinder(nm, r=0.32, h=z1 - z0, loc=(R1 - 0.12, 0, z0), material=WHITE, seg=16)# ---------------------------------------------------------------- F-1 엔진def f1_engine(): # 노즐 연장부(검은 아래쪽)와 재생 냉각 노즐·연소실(위쪽 강철)을 두 회전체로 나눈다 ext = [(1.88, 0.0), (1.81, 0.55), (1.69, 1.2), (1.55, 1.8), (1.45, 2.15)] up = [(1.45, 2.15), (1.30, 2.65), (1.07, 3.25), (0.84, 3.75), (0.60, 4.15), (0.49, 4.4), (0.56, 4.7), (0.60, 5.1), (0.52, 5.5), (0, 5.6)] inner = [(r - 0.05, z) for r, z in reversed(ext)] nz = lathe('F1_Nozzle_Extension', [(0, 2.1)] + inner + [(1.88, -0.02)] + ext[1:], material=NOZZLE, seg=64) parts = [nz, lathe('F1_Thrust_Chamber', up, material=PUMP, seg=64)] parts.append(torus('F1_ExhaustManifold', R=1.47, r=0.12, loc=(0, 0, 2.15), material=NOZZLE, seg=64, rseg=10)) parts.append(torus('F1_ChamberRing', R=0.68, r=0.08, loc=(0, 0, 4.6), material=PUMP, seg=48, rseg=8)) # 터보펌프 + 열교환기 + 배관 tp = cylinder('F1_Turbopump', r=0.42, h=1.5, loc=(0.95, 0, 3.9), material=PUMP, seg=24) parts.append(tp) parts.append(cylinder('F1_TurbineExhaustDuct', r=0.16, h=1.4, loc=(1.15, 0, 2.3), rot=(0, 0.25, 0), material=NOZZLE, seg=16)) parts.append(cylinder('F1_HeatExchanger', r=0.28, h=0.7, loc=(0.95, 0, 5.4), material=PUMP, seg=20)) for k, a in enumerate((1.6, -1.6)): parts.append(cylinder(f'F1_PropLine{k}', r=0.17, h=1.3, loc=(0.55 * math.cos(a), 0.55 * math.sin(a), 4.3), material=PUMP, seg=14)) parts.append(box('F1_GimbalBlock', size=(0.7, 0.7, 0.4), loc=(0, 0, 5.75), material=PUMP)) return join(parts, 'F1_Engine')eng = f1_engine()ENG_R = 3.75eng.name = 'F1_Engine_Center'outer_engines = []for k in range(4): a = math.radians(45 + 90 * k) e = duplicate(eng, loc=(ENG_R * math.cos(a), ENG_R * math.sin(a), 0), rot=(0, 0, a + math.pi), name=f'F1_Engine_Outer_{k + 1}') outer_engines.append(e)eng.rotation_euler.z = math.radians(30)# ---------------------------------------------------------------- 엔진 페어링과 날개fair_prof = [(1.95, 4.45), (2.12, 4.05), (2.16, 4.3), (2.08, 5.6), (1.86, 7.3), (1.52, 9.2), (1.10, 11.2), (0.62, 13.2), (0.25, 14.6), (0, 15.0)]fairing = lathe('Engine_Fairing', fair_prof, material=WHITE, seg=48)fin_pts = [(4.9, 4.25), (9.6, 4.75), (9.6, 6.35), (4.9, 11.6)]fin = extrude_profile('Fin', fin_pts, 0.42, material=WHITE)bevel(fin, 0.08, 2)for k in range(4): a = math.radians(45 + 90 * k) f = fairing if k == 0 else duplicate(fairing, name='Engine_Fairing') f.location = (ENG_R * math.cos(a), ENG_R * math.sin(a), 0) f.name = f'Engine_Fairing_{k + 1}' fn = fin if k == 0 else duplicate(fin) fn.rotation_euler = (0, 0, a) fn.name = f'Fin_{k + 1}'# 얼리지(분리 보조) 모터 덮개: S-IC/S-II 단간부 8개ull = lathe('Ullage_Fairing', [(0.0, 0.0), (0.34, 0.25), (0.36, 2.2), (0.0, 3.0)], material=WHITE, seg=16)ull.scale.y = 0.7for k in range(8): a = math.radians(22.5 + 45 * k) u = ull if k == 0 else duplicate(ull) u.location = ((R1 - 0.08) * math.cos(a), (R1 - 0.08) * math.sin(a), 43.0) u.rotation_euler.z = a u.name = f'Ullage_Motor_Fairing_{k + 1}'# S-IVB 보조 추진(APS) 모듈 2개for k, a in enumerate((math.radians(0), math.radians(180))): m = box(f'SIVB_APS_Module_{k + 1}', size=(0.9, 1.5, 2.6), loc=((R3 + 0.3) * math.cos(a), (R3 + 0.3) * math.sin(a), 72.4), rot=(0, 0, a), material=BLACK, base=True) bevel(m, 0.18, 3)# SLA 패널 이음선 4줄for k in range(4): a = math.radians(90 * k) p0 = ((R3 + 0.03) * math.cos(a), (R3 + 0.03) * math.sin(a), 85.95) p1 = ((RSM + 0.03) * math.cos(a), (RSM + 0.03) * math.sin(a), 94.35) d = Vector(p1) - Vector(p0) sl = cylinder(f'SLA_Panel_Seam_{k + 1}', r=0.05, h=d.length, loc=p0, material=IU, seg=8) sl.rotation_euler = Vector((0, 0, 1)).rotation_difference(d).to_euler()# ---------------------------------------------------------------- 기계선 RCS 쿼드quad = box('SM_RCS_Quad', size=(0.35, 0.7, 0.9), loc=(0, 0, 0), material=SILVER)bevel(quad, 0.05, 2)for k in range(4): a = math.radians(45 + 90 * k) q = quad if k == 0 else duplicate(quad) q.location = ((RSM + 0.12) * math.cos(a), (RSM + 0.12) * math.sin(a), 96.6) q.rotation_euler.z = a q.name = f'SM_RCS_Quad_{k + 1}' for s in (-1, 1): n = cone(f'SM_RCS_Thruster_{k + 1}', r=0.07, h=0.2, loc=((RSM + 0.2) * math.cos(a), (RSM + 0.2) * math.sin(a), 96.6 + s * 0.45), rot=(0 if s > 0 else math.pi, 0, 0), material=NOZZLE, seg=10)# ---------------------------------------------------------------- 발사 탈출 시스템(LES)def strut(name, p0, p1, r, material): p0, p1 = Vector(p0), Vector(p1) d = p1 - p0 ob = cylinder(name, r=r, h=d.length, loc=p0, material=material, seg=8) ob.rotation_euler = Vector((0, 0, 1)).rotation_difference(d).to_euler() return obT0, T1 = 101.0, 104.8legs = []w0, w1 = 0.62, 0.36corners = [(1, 1), (-1, 1), (-1, -1), (1, -1)]lev = [T0, T0 + 1.27, T0 + 2.53, T1]def cp(c, z): w = w0 + (w1 - w0) * (z - T0) / (T1 - T0) return (c[0] * w, c[1] * w, z)tower = []for c in corners: tower.append(strut('LES_Tower_Leg', cp(c, T0 - 0.3), cp(c, T1), 0.06, LES_RED))for i in range(4): c0, c1 = corners[i], corners[(i + 1) % 4] for b in range(3): za, zb = lev[b], lev[b + 1] tower.append(strut('LES_Tower_Brace', cp(c0, za), cp(c1, zb), 0.035, LES_RED)) tower.append(strut('LES_Tower_Brace', cp(c1, za), cp(c0, zb), 0.035, LES_RED)) for b in range(4): tower.append(strut('LES_Tower_Ring', cp(c0, lev[b]), cp(c1, lev[b]), 0.035, LES_RED))join(tower, 'LES_Tower_Truss')lathe('LES_Escape_Motor', [(0, T1 - 0.05), (0.40, T1 - 0.05), (0.40, T1 + 0.25), (0.33, T1 + 0.6), (0.33, 108.6), (0.30, 109.0), (0.20, 109.0), (0.20, 109.6), (0.15, 110.25), (0.06, 110.5), (0, 110.6)], material=WHITE, seg=32)lathe('LES_Nose_Cone', [(0, 108.95), (0.31, 108.95), (0.31, 109.25), (0.21, 109.9), (0.11, 110.35), (0.0, 110.42)], material=BLACK, seg=32)for k in range(4): a = math.radians(45 + 90 * k) n = cylinder(f'LES_Motor_Nozzle_{k + 1}', r=0.13, r2=0.07, h=0.45, loc=(0.30 * math.cos(a), 0.30 * math.sin(a), T1 + 0.35), material=NOZZLE, seg=12, base=False) # 노즐 축(로컬 +Z, 좁은 쪽)이 위·안쪽을 향하도록 기울인다 → 넓은 출구가 바깥 아래로 n.rotation_euler = Vector((0, 0, 1)).rotation_difference(Vector((-math.cos(a) * 0.55, -math.sin(a) * 0.55, 1)).normalized()).to_euler()# ---------------------------------------------------------------- 성조기와 USA 글자 (곡면에 감기)def wrap_mesh(me, R, theta_c, z_c, lift=0.012): for v in me.vertices: u, w, d = v.co ang = theta_c + u / R rr = R + lift + d v.co = (rr * math.cos(ang), rr * math.sin(ang), z_c + w) me.update()def wrapped_text(name, body, size, R, theta_c, z_c, depth, material): t = text3d(name + '_src', body, size=size, depth=depth) t.data.align_y = 'CENTER' t.data.offset = size * 0.035 # 글자 획을 굵게 dg = bpy.context.evaluated_depsgraph_get() me = bpy.data.meshes.new_from_object(t.evaluated_get(dg)) bpy.data.objects.remove(t, do_unlink=True) for v in me.vertices: v.co.z += depth # 뒷면을 0에 맞춘다 wrap_mesh(me, R, theta_c, z_c) me.materials.append(material) ob = bpy.data.objects.new(name, me) link(ob) return obFRONT = math.radians(-90)for k, ch in enumerate('USA'): wrapped_text(f'Lettering_{ch}', ch, 3.3, R1, FRONT, 34.6 - k * 3.1, 0.03, BLACK)def flag(name, R, theta_c, z_c, wid, hgt): bm = bmesh.new() cols, rows = 24, 13 vs = [[bm.verts.new((-wid / 2 + wid * i / cols, hgt / 2 - hgt * j / rows, 0)) for i in range(cols + 1)] for j in range(rows + 1)] for j in range(rows): for i in range(cols): f = bm.faces.new((vs[j + 1][i], vs[j + 1][i + 1], vs[j][i + 1], vs[j][i])) if j < 7 and i < 10: f.material_index = 2 else: f.material_index = 0 if j % 2 == 0 else 1 me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() wrap_mesh(me, R, theta_c, z_c, lift=0.02) for m in (RED, WHITE, BLUE): me.materials.append(m) ob = bpy.data.objects.new(name, me) link(ob) return obflag('US_Flag_SIC', R1, FRONT, 37.55, 4.4, 2.4)# 발사탑 연결(엄빌리컬) 접속판 — 한쪽 옆면을 따라 단마다 하나씩UMB = math.radians(150)for nm, r, z, w, h in [('Umbilical_Plate_SIC', R1, 40.4, 1.0, 0.8), ('Umbilical_Plate_SII_Aft', R1, 49.5, 1.0, 0.8), ('Umbilical_Plate_SII_Fwd', R1, 64.6, 1.0, 0.8), ('Umbilical_Plate_SIVB', R3, 79.0, 0.8, 0.7), ('Umbilical_Plate_IU', R3, 85.45, 0.7, 0.5), ('Umbilical_Plate_SM', RSM, 95.6, 0.5, 0.5)]: p = box(nm, size=(0.24, w, h), loc=(r * math.cos(UMB), r * math.sin(UMB), z), rot=(0, 0, UMB), material=HEAT) bevel(p, 0.03, 1)# ---------------------------------------------------------------- 이동식 발사대(ML) 갑판과 고정 팔# 실제 갑판은 약 49m x 41m, 두께 7.6m 이고 가운데 13.7m 정사각 배기 구멍 위에 로켓이 선다. 엔진은 구멍 안으로 내려간다.ML_TOP, ML_H, ML_X, ML_Y, HOLE = 2.0, 7.6, 49.0, 41.0, 13.7def launcher_deck(name): bm = bmesh.new() ho, hx, hy = HOLE / 2, ML_X / 2, ML_Y / 2 outer = [(-hx, -hy), (hx, -hy), (hx, hy), (-hx, hy)] inner = [(-ho, -ho), (ho, -ho), (ho, ho), (-ho, ho)] z0, z1 = ML_TOP - ML_H, ML_TOP ot = [bm.verts.new((x, y, z1)) for x, y in outer] ob_ = [bm.verts.new((x, y, z0)) for x, y in outer] it = [bm.verts.new((x, y, z1)) for x, y in inner] ib = [bm.verts.new((x, y, z0)) for x, y in inner] for i in range(4): k = (i + 1) % 4 bm.faces.new((ot[i], ot[k], it[k], it[i])) # 윗면 bm.faces.new((ob_[k], ob_[i], ib[i], ib[k])) # 밑면 bm.faces.new((ob_[i], ob_[k], ot[k], ot[i])) # 바깥 옆면 bm.faces.new((it[i], it[k], ib[k], ib[i])) # 구멍 안쪽 면 bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() me.materials.append(ML_STEEL) ob = bpy.data.objects.new(name, me) link(ob) return oblauncher_deck('Mobile_Launcher_Deck')# 고정(홀드다운) 팔: 갑판 윗면에서 1단 바닥(Z_BASE)까지 받친다arm = box('HoldDown_Arm', size=(2.2, 1.4, Z_BASE - ML_TOP), loc=(0, 0, ML_TOP), material=ML_STEEL, base=True)bevel(arm, 0.08, 1)for k in range(4): a = math.radians(90 * k) h = arm if k == 0 else duplicate(arm) h.location = ((R1 + 0.2) * math.cos(a), (R1 + 0.2) * math.sin(a), ML_TOP) h.rotation_euler.z = a h.name = f'HoldDown_Arm_{k + 1}'# 꼬리 서비스 마스트 3기: 갑판 위에 선 철탑 상자(연료·전원 연결용)tsm = box('Tail_Service_Mast', size=(2.4, 2.4, 6.5), loc=(0, 0, ML_TOP), material=LES_RED, base=True)bevel(tsm, 0.1, 1)for k, a in enumerate((math.radians(110), math.radians(160), math.radians(-160))): t = tsm if k == 0 else duplicate(tsm) t.location = (11.5 * math.cos(a), 11.5 * math.sin(a), ML_TOP) t.rotation_euler.z = a t.name = f'Tail_Service_Mast_{k + 1}'






