
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
우주왕복선은 미국 항공우주국(NASA)이 1981년부터 2011년까지 운용한 재사용 유인 우주 수송 체계다. 1981년 4월 STS-1 비행을 시작으로 2011년 7월 STS-135 까지 135회 비행했으며, 위성 배치와 허블 우주망원경 수리, 국제우주정거장 조립에 쓰였다.
발사대에 선 모습은 세 부분으로 이루어진다. 날개 달린 오비터가 지름 8.4m 의 외부연료탱크(ET) 배에 붙고, 탱크 양옆에 고체로켓부스터(SRB) 두 기가 선다. 오비터의 주 엔진 3기는 탱크의 액체산소·액체수소를 받아 연소하고, 부스터는 이륙 후 약 2분 동안 추력 대부분을 낸 뒤 분리된다. 단열 폼으로 덮인 외부연료탱크의 주황색은 초기 두 차례 비행 이후 흰 도장을 생략하면서 굳어진 색이다.
이 모델은 발사 직전의 결합 형상을 실제 치수에 맞춰 세웠다. 오비터의 이중 삼각익과 수직꼬리날개, OMS 포드와 주 엔진 노즐, 배면의 검은 내열 타일과 윗면의 흰 타일, 조종석 창, 외부연료탱크의 오자이브 머리와 주름진 인터탱크, LOX 피드라인, 부스터의 이음 링과 스커트, 그리고 세 부분을 잇는 결합 스트럿을 표현했다.
만든 방법
오비터 동체는 길이 방향 단면표(반폭·바닥·지붕 높이)를 단조 3차 보간해 수십 개의 슈퍼타원 단면을 만들고 이를 이어 붙인 로프트 메시다. 단면 링마다 흑백 타일 경계 높이에 정점을 정확히 두어 경계선이 계단 없이 이어지게 했고, 창문과 검은 마스크는 동체 표면을 몇 cm 띄워 따라가는 패치로 붙였다. 날개와 수직꼬리날개는 평면형 위에 NACA 4자리형 두께 분포를 얹어 bmesh 로 짰으며, 시위 분할에 고정 인덱스를 두어 엘레본·러더 힌지선을 면 재질로 칠했다. 외부연료탱크·부스터·엔진 노즐은 회전체(lathe)이고 인터탱크 스트링거는 반경을 번갈아 바꾼 링으로 표현했다. 타일 한 장 한 장, 부스터 표면의 글자와 깃발, 배관 세부는 생략했고 탱크와 부스터의 높이 관계는 사진을 바탕으로 근사했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 520줄.
# 우주왕복선 발사 형상(STS 스택) — 오비터 + 외부연료탱크(ET) + 고체로켓부스터(SRB) 2기.# 오비터 동체는 단면표(폭·바닥·지붕 높이)를 보간해 수십 개의 슈퍼타원 단면을 이어 붙인 로프트 메시이고,# 이중 삼각익과 수직꼬리날개는 평면형 위에 익형 두께 분포를 얹어 bmesh 로 직접 짰다.# 단열 타일의 흑백 경계, 창문, 앞전 RCC 는 면 위치에 따라 재질 슬롯을 나눠 칠했다.# ET·SRB·엔진 노즐은 회전체(lathe), 인터탱크 주름과 SRB 이음 링은 루프로 만들었다. 단위 m, 실제 크기.import mathfrom atelier import *SHOT = {"azimuth": -70, "elevation": 8, "lens": 60, "light": "day", "background": "#1d2533"}# ---------------------------------------------------------------- 재질M_WHITE = mat('Orbiter_White_Tiles', '#e6e4dd', 0.62)M_BLACK = mat('Orbiter_Black_Tiles', '#222326', 0.75)M_RCC = mat('RCC_Leading_Edge_Grey', '#5c5e61', 0.6)M_GLASS = mat('Cockpit_Window', '#1a2530', 0.08, 0.3)M_BASE = mat('Aft_Heat_Shield', '#3b3936', 0.8)M_SSME = mat('SSME_Nozzle_Metal', '#a39a8a', 0.35, 0.85)M_ET = mat('ET_Foam_Orange', '#ad5b2b', 0.85)M_IT = mat('ET_Intertank_Foam', '#9b4f28', 0.85)M_SRB = mat('SRB_White_Paint', '#e2e0d8', 0.5)M_JOINT = mat('SRB_Joint_Band', '#9c9b96', 0.55, 0.2)M_SRBN = mat('SRB_Nozzle_Ablative', '#4a4643', 0.8)M_STRUT = mat('Attach_Strut_Metal', '#8d9095', 0.4, 0.7)M_SEAM = mat('Panel_Seam_Grey', '#6f6e6a', 0.7)def build_mesh(name, verts, faces, mats=None, mat_idx=None, smooth_all=True): me = bpy.data.meshes.new(name) me.from_pydata(verts, [], faces) me.validate() ob = bpy.data.objects.new(name, me) link(ob) for m in (mats or []): me.materials.append(m) if mat_idx is not None: me.polygons.foreach_set('material_index', mat_idx) if smooth_all: for p in me.polygons: p.use_smooth = True return obdef pchip(xs, ys): """단조 3차 보간 함수(과도한 출렁임 없이 단면표를 매끄럽게).""" n = len(xs) h = [xs[i + 1] - xs[i] for i in range(n - 1)] d = [(ys[i + 1] - ys[i]) / h[i] for i in range(n - 1)] m = [0.0] * n나머지 472줄 펼치기접기
m[0], m[-1] = d[0], d[-1] for i in range(1, n - 1): if d[i - 1] * d[i] <= 0: m[i] = 0.0 else: w1, w2 = 2 * h[i] + h[i - 1], h[i] + 2 * h[i - 1] m[i] = (w1 + w2) / (w1 / d[i - 1] + w2 / d[i]) def f(x): if x <= xs[0]: return ys[0] if x >= xs[-1]: return ys[-1] i = max(k for k in range(n - 1) if xs[k] <= x) t = (x - xs[i]) / h[i] h00, h10 = 2 * t ** 3 - 3 * t ** 2 + 1, t ** 3 - 2 * t ** 2 + t h01, h11 = -2 * t ** 3 + 3 * t ** 2, t ** 3 - t ** 2 return h00 * ys[i] + h10 * h[i] * m[i] + h01 * ys[i + 1] + h11 * h[i] * m[i + 1] return fdef spow(x, p): return math.copysign(abs(x) ** p, x)# ================================================================ 오비터 (로컬 좌표: x=기수 방향 u, y=우익 v, z=지붕 w)orbiter = bpy.data.objects.new('Orbiter', None)link(orbiter)ORB_PARTS = []# 단면표: u, 반폭, 바닥높이, 지붕높이ST = [ (0.0, 2.65, 0.00, 5.30), (21.0, 2.65, 0.00, 5.30), (23.0, 2.65, 0.00, 5.45), (25.0, 2.65, 0.03, 5.95), (26.3, 2.62, 0.07, 6.05), (27.5, 2.52, 0.16, 5.62), (28.6, 2.30, 0.30, 4.72), (29.8, 1.95, 0.52, 4.05), (30.9, 1.50, 0.82, 3.45), (31.8, 1.00, 1.12, 2.95), (32.4, 0.58, 1.42, 2.55), (32.8, 0.12, 1.80, 2.15),]f_hw = pchip([s[0] for s in ST], [s[1] for s in ST])f_wb = pchip([s[0] for s in ST], [s[2] for s in ST])f_wt = pchip([s[0] for s in ST], [s[3] for s in ST])NOSE = 32.9def black_line(u): """옆면 흑백 타일 경계 높이(기수 쪽으로 올라간다).""" if u < 22: return 1.05 t = min(1.0, (u - 22) / (NOSE - 22)) return 1.05 + 1.25 * t * tdef sec(u): hw, wb, wt = f_hw(u), f_wb(u), f_wt(u) return hw, wb, wt, wb + 0.36 * (wt - wb)def fus_point(u, a, grow=0.0): """동체 단면(u, 각도 a) 위의 점. 각도 0=우측, 90°=지붕. grow 만큼 바깥으로.""" hw, wb, wt, wc = sec(u) hw, wb, wt = hw + grow, wb - grow, wt + grow c, s_ = math.cos(a), math.sin(a) y = hw * spow(c, 0.30 if s_ < 0 else 0.55) z = wc + (wt - wc) * spow(s_, 0.62) if s_ >= 0 else wc + (wc - wb) * spow(s_, 0.25) return (u, y, z)def boundary_angle(u): hw, wb, wt, wc = sec(u) zb = min(black_line(u), wb + 0.8 * (wt - wb)) if zb < wc: return -math.asin(min(1.0, ((wc - zb) / (wc - wb)) ** 4)) return math.asin(min(1.0, ((zb - wc) / (wt - wc)) ** (1 / 0.62)))NB, NT = 14, 18 # 반쪽 링의 아래(검은 타일)/위(흰 타일) 분할 수def ring_angles(u): ab = boundary_angle(u) out = [-math.pi / 2 + (ab + math.pi / 2) * k / NB for k in range(NB)] out += [ab + (math.pi / 2 - ab) * k / NT for k in range(NT)] return out + [math.pi / 2] + [math.pi - a for a in reversed(out[1:])]def fuselage(): us = [] u = 0.0 while u < NOSE - 0.05: us.append(u) u += 0.9 if u < 20 else (0.35 if u < 26 else 0.16) us.append(NOSE - 0.04) N = 2 * (NB + NT) verts, faces, mi = [], [], [] for u in us: for a in ring_angles(u): verts.append(fus_point(u, a)) R = len(us) for i in range(R - 1): for k in range(N): a0, a1 = i * N + k, i * N + (k + 1) % N faces.append((a0, a1, a1 + N, a0 + N)) cu = (us[i] + us[i + 1]) / 2 bottom = k < NB or k >= N - NB m = 1 if bottom else 0 if cu > NOSE - 0.75: m = 2 # 기수 RCC mi.append(m) tip = len(verts) verts.append((NOSE, 0.0, f_wb(NOSE) * 0.5 + f_wt(NOSE) * 0.5)) for k in range(N): faces.append(((R - 1) * N + k, (R - 1) * N + (k + 1) % N, tip)) mi.append(2) faces.append(tuple(reversed(range(N)))) mi.append(4) return build_mesh('Orbiter_Fuselage', verts, faces, [M_WHITE, M_BLACK, M_RCC, M_GLASS, M_BASE], mi)def surface_patch(name, u0, u1, a0, a1, grow, material, nu=8, na=10): """동체 표면을 grow 만큼 띄워 따라가는 얇은 패널(창문·검은 마스크).""" verts, faces = [], [] for i in range(nu + 1): u = u0 + (u1 - u0) * i / nu for j in range(na + 1): verts.append(fus_point(u, a0 + (a1 - a0) * j / na, grow)) for i in range(nu): for j in range(na): a = i * (na + 1) + j faces.append((a, a + na + 1, a + na + 2, a + 1)) return build_mesh(name, verts, faces, [material])ORB_PARTS.append(fuselage())D = math.radiansORB_PARTS.append(surface_patch('Cockpit_Black_Mask', 27.25, 28.95, D(34), D(146), 0.008, M_BLACK, 12, 28))for k in range(3): lo = 50 + k * 13.5 for a0, a1 in ((lo, lo + 12), (180 - lo - 12, 180 - lo)): ORB_PARTS.append(surface_patch('Forward_Windshield', 27.62, 28.62, D(a0), D(a1), 0.018, M_GLASS, 6, 4))for a0, a1 in ((74, 87), (93, 106)): ORB_PARTS.append(surface_patch('Overhead_Window', 26.95, 27.45, D(a0), D(a1), 0.016, M_GLASS, 4, 4))for a0, a1 in ((20, 36), (144, 160)): ORB_PARTS.append(surface_patch('Side_Hatch_Window', 26.35, 26.95, D(a0), D(a1), 0.016, M_GLASS, 4, 4))def airfoil_t(xa): """앞전 0 → 뒷전 1 의 두께 분포(NACA 4자리형).""" x = xa return 1.0 * (0.2969 * math.sqrt(x) - 0.1260 * x - 0.3516 * x * x + 0.2843 * x ** 3 - 0.1036 * x ** 4) / 0.5 + 0.012def hinge_xs(K1, K2, xh, g=0.006): """앞전 0→뒷전 1 시위 분할. 인덱스 K1~K1+1 사이가 힌지선 띠가 되도록 고정.""" a = [(xh - g) * (1 - math.cos(math.pi / 2 * k / K1)) for k in range(K1 + 1)] b = [xh + g + (1 - xh - g) * k / K2 for k in range(K2 + 1)] return a + bdef wings(): # 평면형: 스트레이크(81°) + 주익(45°), 반스팬 11.9 m def le(v): return 24.5 - (v - 2.6) * math.tan(math.radians(81)) if v <= 4.7 else 11.2 - (v - 4.7) def te(v): return 0.25 + 0.05 * (v - 2.6) vs = [2.25, 2.6, 3.1, 3.6, 4.2, 4.7, 5.3, 6.0, 6.6, 7.22, 7.32, 8.0, 9.0, 10.0, 10.8, 11.4, 11.9] K1, K2 = 20, 5 K = K1 + K2 + 1 verts, faces, mi = [], [], [] loop_n = 2 * K for side in (1, -1): base = len(verts) for v in vs: u_le, u_te = le(v), te(v) chord = u_le - u_te u_lem = 4.0 + (11.9 - v) # 주익 앞전 연장선 T = 0.12 * (u_lem - u_te) w0 = 0.32 + (v - 2.6) * 0.058 xs = hinge_xs(K1, K2, 1 - 2.3 / chord) ring = [] for k in range(loop_n): # 0..K 윗면(앞전→뒷전), K+1..2K-1 아랫면(뒷전→앞전) xa = xs[k] if k <= K else xs[2 * K - k] u = u_le - xa * chord xm = (u_lem - u) / (u_lem - u_te) if xm < 0: # 스트레이크 구간은 얇게 th = T * airfoil_t(min(1, xa * chord / 3.0)) * max(0.22, 0.55 + 0.04 * xm) else: th = T * airfoil_t(min(1.0, max(0.0, xm))) if xa * chord > 0.6 else T * airfoil_t(xm if xm > 0 else 0) * math.sqrt(xa * chord / 0.6) z = w0 + 0.8 * th if k <= K else w0 - 0.2 * th ring.append((u, side * v, z)) verts.extend(ring) S = len(vs) for i in range(S - 1): for k in range(loop_n): a0 = base + i * loop_n + k a1 = base + i * loop_n + (k + 1) % loop_n f = (a0, a1, a1 + loop_n, a0 + loop_n) faces.append(f if side == 1 else tuple(reversed(f))) kk = k + 0.5 vm = (vs[i] + vs[i + 1]) / 2 if kk < K: xa = xs[k] m = 2 if xa < 0.035 else (1 if xa < 0.09 else 0) if vm > 3.3 and (k == K1 or (k > K1 and 7.22 < vm < 7.32)): m = 3 # 엘레본 힌지·분할선 mi.append(m) else: xa = xs[2 * K - k - 1] if k < 2 * K - 1 else 0 mi.append(2 if xa < 0.02 else 1) tipring = list(range(base + (S - 1) * loop_n, base + S * loop_n)) rootring = list(range(base, base + loop_n)) faces.append(tuple(tipring) if side == -1 else tuple(reversed(tipring))) mi.append(2) faces.append(tuple(rootring) if side == 1 else tuple(reversed(rootring))) mi.append(1) ob = build_mesh('Orbiter_Wings', verts, faces, [M_WHITE, M_BLACK, M_RCC, M_SEAM], mi) return obORB_PARTS.append(wings())def vertical_tail(): ws = [4.9, 5.4, 6.5, 7.6, 8.8, 10.0, 11.2, 12.4, 13.4, 14.3] K1, K2 = 13, 6 K = K1 + K2 + 1 xs = hinge_xs(K1, K2, 0.64, 0.004) verts, faces, mi = [], [], [] loop_n = 2 * K for w in ws: u_le = 8.8 - (w - 5.4) * 1.0 u_te = -1.0 - (w - 5.4) * 0.19 chord = u_le - u_te T = 0.075 * chord for k in range(loop_n): xa = xs[k] if k <= K else xs[2 * K - k] side = 1 if k <= K else -1 th = T * airfoil_t(xa) * 0.5 verts.append((u_le - xa * chord, side * th, w)) S = len(ws) for i in range(S - 1): for k in range(loop_n): a0 = i * loop_n + k a1 = i * loop_n + (k + 1) % loop_n faces.append((a0, a0 + loop_n, a1 + loop_n, a1)) j = k if k < K else 2 * K - k - 1 m = 2 if xs[j] < 0.03 else 0 if j == K1 and ws[i] >= 5.4: m = 3 # 러더/스피드브레이크 힌지선 mi.append(m) faces.append(tuple(range((S - 1) * loop_n, S * loop_n))) mi.append(2) faces.append(tuple(reversed(range(loop_n)))) mi.append(0) return build_mesh('Orbiter_Vertical_Tail', verts, faces, [M_WHITE, M_BLACK, M_RCC, M_SEAM], mi)ORB_PARTS.append(vertical_tail())def oms_pod(side): us = [-0.9, -0.75, -0.5, 0.0, 2.0, 4.0, 5.5, 7.0, 8.5, 9.8, 10.8] sc = [0.45, 0.82, 0.95, 1.0, 1.0, 0.98, 0.9, 0.74, 0.52, 0.3, 0.06] N = 32 verts, faces = [], [] for u, s in zip(us, sc): for k in range(N): a = 2 * math.pi * k / N verts.append((u, side * 1.95 + 1.15 * s * math.cos(a), 4.8 + 1.15 * s * math.sin(a) - (1 - s) * 0.45)) R = len(us) for i in range(R - 1): for k in range(N): a0, a1 = i * N + k, i * N + (k + 1) % N f = (a0, a1, a1 + N, a0 + N) faces.append(f if side > 0 else tuple(reversed(f))) faces.append(tuple(reversed(range(N))) if side > 0 else tuple(range(N))) faces.append(tuple(range((R - 1) * N, R * N)) if side > 0 else tuple(reversed(range((R - 1) * N, R * N)))) ob = build_mesh('Orbiter_OMS_Pod_' + ('R' if side > 0 else 'L'), verts, faces, [M_WHITE]) return obfor sd in (1, -1): ORB_PARTS.append(oms_pod(sd))def bell(name, r0, r1, L, material, seg=40): prof = [] n = 14 for i in range(n + 1): t = i / n prof.append((r0 + (r1 - r0) * (t ** 0.62), -L * t)) ob = lathe(name, prof, material=material, seg=seg) solidify(ob, 0.04) return ob# 주 엔진(SSME) 3기: 위 1, 아래 2 — 노즐은 -u 방향ssme_proto = bell('SSME_Nozzle', 0.52, 1.15, 3.0, M_SSME)ssme_proto.rotation_euler = (0, math.radians(-90), 0)ENG_POS = [(0.0, 4.1), (1.38, 1.85), (-1.38, 1.85)]for i, (v, w) in enumerate(ENG_POS): ob = ssme_proto if i == 0 else duplicate(ssme_proto, name='SSME_Nozzle') ob.location = (-0.55, v, w) mount = cylinder('SSME_Powerhead_Mount', r=0.62, h=0.6, loc=(-0.6, v, w), rot=(0, math.radians(90), 0), material=M_BASE, seg=24) ORB_PARTS += [ob, mount]# OMS 엔진 노즐oms_n = bell('OMS_Nozzle', 0.28, 0.55, 1.3, M_SSME, seg=28)oms_n.rotation_euler = (0, math.radians(-90), 0)oms_n.location = (-0.85, 1.95, 4.85)ORB_PARTS += [oms_n, duplicate(oms_n, loc=(-0.85, -1.95, 4.85))]# 보디 플랩flap = prism('Orbiter_Body_Flap', [(-2.55, -3.0), (0.1, -3.05), (0.1, 3.05), (-2.55, 3.0)], 0.5, loc=(0, 0, 0.06), material=M_BLACK)ORB_PARTS.append(flap)# 화물칸 도어: 가로 이음선 5줄, 힌지선 2줄, 중앙선 1줄A_HINGE = math.radians(28)for i, u in enumerate((4.2, 8.75, 13.3, 17.85, 22.4)): pts = [fus_point(u, A_HINGE + (math.pi - 2 * A_HINGE) * k / 24, 0.012) for k in range(25)] ORB_PARTS.append(curve_tube('Payload_Bay_Door_Seam', pts, radius=0.03, material=M_SEAM, bevel_res=1))for a in (A_HINGE, math.pi - A_HINGE, math.pi / 2): pts = [fus_point(4.2 + 18.2 * k / 20, a, 0.012) for k in range(21)] ORB_PARTS.append(curve_tube('Payload_Bay_Hinge_Line', pts, radius=0.03, material=M_SEAM, bevel_res=1))parent_to(ORB_PARTS, orbiter)# ================================================================ 스택 배치(월드)ET_R = 4.2ET_Z0 = 8.0ORB_X = ET_R + 1.35 # 오비터 배면(w=0)의 XORB_Z = 6.6 # 오비터 후미(u=0)의 Zorbiter.matrix_world = Matrix(((0, 0, 1, ORB_X), (0, -1, 0, 0), (1, 0, 0, ORB_Z), (0, 0, 0, 1)))# ================================================================ 외부연료탱크(ET)lh2 = [(0.0, ET_Z0)]for i in range(1, 9): t = i / 8 * math.pi / 2 lh2.append((ET_R * math.sin(t), ET_Z0 + 1.7 * (1 - math.cos(t))))lh2 += [(ET_R, 33.5), (0.0, 33.5)]et_lh2 = lathe('ET_LH2_Tank', lh2, material=M_ET, seg=96)lox = [(0.0, 40.2), (ET_R, 40.2), (ET_R, 42.0)]L, R = 12.3, ET_Rrho = (R * R + L * L) / (2 * R)for i in range(1, 25): zz = L * i / 24 r = math.sqrt(max(0.0, rho * rho - zz * zz)) + R - rho if r < 0.62: break lox.append((r, 42.0 + zz))ztop = lox[-1][1]lox += [(0.55, ztop + 0.05), (0.22, ztop + 0.9), (0.06, ztop + 1.25), (0.0, ztop + 1.3)]et_lox = lathe('ET_LOX_Tank', lox, material=M_ET, seg=96)# 인터탱크: 주름(스트링거) 단면def intertank(): N = 216 z0, z1 = 33.35, 40.35 verts, faces = [], [] for z in (z0, z1): for k in range(N): a = 2 * math.pi * k / N r = ET_R + (0.07 if (k // 2) % 3 == 0 else 0.03) verts.append((r * math.cos(a), r * math.sin(a), z)) for k in range(N): faces.append((k, (k + 1) % N, N + (k + 1) % N, N + k)) return build_mesh('ET_Intertank', verts, faces, [M_IT], smooth_all=False)et_it = intertank()for zz in (33.4, 40.3): torus('ET_Flange_Ring', R=ET_R + 0.06, r=0.09, loc=(0, 0, zz), material=M_IT, seg=96, rseg=8)# ================================================================ 고체로켓부스터(SRB)SRB_R = 1.855SRB_Y = ET_R + SRB_R + 0.95srb_prof = [(0.0, 3.0), (1.72, 3.0), (2.5, 0.0), (2.66, 0.0), (2.62, 0.3), (1.87, 3.4), (SRB_R, 3.45), (SRB_R, 39.0)]for i in range(1, 7): t = i / 6 srb_prof.append((SRB_R - 0.33 * t, 39.0 + 1.9 * t))zc = 40.9for i in range(1, 13): t = i / 12 r = 1.525 * (1 - t) ** 1.25 + 0.1 * (1 - t) srb_prof.append((max(r, 0.12), zc + 4.35 * t))srb_prof += [(0.06, 45.36), (0.0, 45.42)]srb_body = lathe('SRB_Case', srb_prof, material=M_SRB, seg=64)srb_noz = bell('SRB_Nozzle', 0.7, 1.75, 2.8, M_SRBN, seg=48)srb_noz.location = (0, 0, 3.0)SRB_PARTS = [srb_body, srb_noz]for zz in (3.45, 11.4, 19.4, 27.4, 35.4, 38.9): SRB_PARTS.append(cylinder('SRB_Field_Joint', r=SRB_R + 0.035, h=0.38, loc=(0, 0, zz - 0.19), material=M_JOINT, seg=64, cap=False))SRB_PARTS.append(cylinder('SRB_Aft_Attach_Ring', r=SRB_R + 0.07, h=0.55, loc=(0, 0, 9.2), material=M_JOINT, seg=64, cap=False))srb_L = bpy.data.objects.new('SRB_Left', None)link(srb_L)parent_to(SRB_PARTS, srb_L)srb_L.location = (0, -SRB_Y, 0)srb_R = bpy.data.objects.new('SRB_Right', None)link(srb_R)srb_R.location = (0, SRB_Y, 0)for p in SRB_PARTS: c = duplicate(p, name=p.name) c.parent = srb_Rsrb_R.rotation_euler = (0, 0, math.pi)# SRB 부속: 시스템 터널, 홀드다운 포스트(로컬 좌표, 좌우 공유)SRB_EXTRA = []ta = math.atan2(-0.87, 0.5)tun = box('SRB_Systems_Tunnel', (0.32, 0.26, 35.4), loc=((SRB_R + 0.1) * math.cos(ta), (SRB_R + 0.1) * math.sin(ta), 3.55), rot=(0, 0, ta), material=M_SRB, base=True)SRB_EXTRA.append(tun)for k in range(4): a = math.pi / 4 + k * math.pi / 2 SRB_EXTRA.append(box('SRB_Holddown_Post', (0.55, 0.45, 0.7), loc=(2.62 * math.cos(a), 2.62 * math.sin(a), 0.0), rot=(0, 0, a), material=M_JOINT, base=True))for p in SRB_EXTRA: p.parent = srb_L c = duplicate(p, name=p.name) c.parent = srb_R# ================================================================ 결합 구조(월드)def strut(name, a, b, r=0.12, material=M_STRUT): return curve_tube(name, [a, b], radius=r, material=material, bevel_res=2)# 오비터 앞 바이포드 / 뒤 결합부strut('ET_Orbiter_Forward_Bipod', (4.05, 1.1, 29.6), (ORB_X + 0.05, 0.0, ORB_Z + 24.3), 0.13)strut('ET_Orbiter_Forward_Bipod', (4.05, -1.1, 29.6), (ORB_X + 0.05, 0.0, ORB_Z + 24.3), 0.13)box('Forward_Bipod_Fitting', (0.6, 0.5, 0.5), loc=(ORB_X - 0.05, 0, ORB_Z + 24.3), material=M_STRUT)torus('ET_Aft_Attach_Ring', R=ET_R + 0.05, r=0.12, loc=(0, 0, 10.4), material=M_STRUT, seg=96, rseg=8)for sy in (1, -1): strut('ET_Orbiter_Aft_Strut', (4.0, sy * 1.3, 10.4), (ORB_X + 0.05, sy * 1.9, ORB_Z + 2.4), 0.16) strut('ET_Orbiter_Aft_Diagonal', (4.0, sy * 1.3, 10.4), (ORB_X + 0.05, sy * 0.4, ORB_Z + 3.4), 0.11)box('ET_Aft_Crossbeam', (0.5, 3.2, 0.45), loc=(4.05, 0, 10.4), material=M_STRUT)# LOX 피드라인(오비터 쪽 +Y) 과 LH2 엄빌리컬(-Y), 케이블 트레이fa = math.radians(22)fx, fy = (ET_R + 0.32) * math.cos(fa), (ET_R + 0.32) * math.sin(fa)curve_tube('ET_LOX_Feedline', [(fx, fy, 36.8), (fx, fy, 11.6), (fx + 0.35, fy, 10.6), (ORB_X - 0.3, fy, ORB_Z + 1.5)], radius=0.22, material=M_ET, bevel_res=3)for zz in range(13, 36, 3): box('ET_Feedline_Bracket', (0.5, 0.18, 0.18), loc=(fx * 0.965, fy * 0.965, zz), rot=(0, 0, fa), material=M_ET)curve_tube('ET_LH2_Umbilical_Line', [(3.35, -1.7, 9.1), (4.4, -1.7, 8.5), (ORB_X - 0.3, -1.7, ORB_Z + 1.5)], radius=0.2, material=M_ET, bevel_res=3)for sy in (1, -1): box('Orbiter_ET_Umbilical_Plate', (0.45, 1.1, 1.3), loc=(ORB_X - 0.15, sy * 1.7, ORB_Z + 1.5), material=M_BASE)ca = math.radians(-24)cx, cy = (ET_R + 0.1) * math.cos(ca), (ET_R + 0.1) * math.sin(ca)box('ET_Cable_Tray', (0.24, 0.42, 29.5), loc=(cx, cy, 10.8), rot=(0, 0, ca), material=M_ET, base=True)# LOX 탱크 오자이브를 타고 오르는 가압 라인pa = math.radians(-24)pts = [(r + 0.16, z) for r, z in lox[2:] if z <= ztop]curve_tube('ET_GOX_Pressurization_Line', [(r * math.cos(pa), r * math.sin(pa), z) for r, z in pts], radius=0.09, material=M_ET, bevel_res=2)# SRB-ET 결합: 앞(인터탱크) 볼트 박스, 뒤 스트럿 3개for sy in (1, -1): box('SRB_Forward_Attach', (0.7, SRB_Y - SRB_R - ET_R + 0.5, 0.6), loc=(0, sy * (ET_R + SRB_Y - SRB_R) / 2, 37.6), material=M_STRUT) for x0, z0 in ((-1.1, 9.5), (0.0, 9.8), (1.1, 9.5)): strut('SRB_Aft_Attach_Strut', (x0, sy * (SRB_Y - math.sqrt(1.95 ** 2 - x0 ** 2) + 0.05), z0), (x0 * 0.85, sy * (math.sqrt(ET_R ** 2 - (x0 * 0.85) ** 2) - 0.08), 10.4), 0.13)






