본문으로 건너뛰기
3D 아틀리에
새턴 V 로켓 3D 모형

끌어서 회전, 휠·두 손가락으로 확대

이 모형에 대하여

새턴 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 ob
Z_BASE = 5.0
HULL = [
(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_W
hull = 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 = a
stringers('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.75
eng.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.7
for 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 ob
T0, T1 = 101.0, 104.8
legs = []
w0, w1 = 0.62, 0.36
corners = [(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 ob
FRONT = 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 ob
flag('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.7
def 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 ob
launcher_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}'

다른 모형