본문으로 건너뛰기
3D 아틀리에
제임스 웹 우주망원경 3D 모형

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

이 모형에 대하여

제임스 웹 우주망원경(JWST)은 미국 항공우주국(NASA)이 유럽우주국(ESA), 캐나다우주국(CSA)과 함께 개발한 적외선 우주망원경이다. 2021년 12월 25일 프랑스령 기아나에서 아리안 5 로켓으로 발사되어, 지구에서 약 150만 km 떨어진 태양-지구 제2 라그랑주점(L2) 부근 궤도에서 관측하고 있다.

주경은 지름 약 6.5m로, 금을 얇게 입힌 베릴륨 육각 세그먼트 18장으로 이루어진다. 로켓 페어링에 들어가도록 좌우 날개를 접어 실었다가 우주에서 펼쳤다. 주경 앞쪽에는 세 개의 지지대가 받치는 부경이 있고, 빛은 주경 가운데 구멍을 지나 뒤쪽의 과학 기기 모듈로 들어간다.

망원경 아래에는 약 21m×14m 크기의 5겹 차양막이 있다. 캡톤 필름으로 된 막이 태양과 지구, 달의 빛과 열을 가려 망원경을 영하 230도 안팎으로 차갑게 유지한다. 그 아래 태양 쪽에는 태양전지판과 안테나를 단 우주선 버스가 있다.

이 모델은 모든 부분을 펼친 관측 상태를 실제 치수에 맞춰 표현했다. 금빛 오목 주경, 부경 삼각 지지대, 겹 사이가 벌어진 보랏빛 차양막, 뒤쪽 끝의 모멘텀 트림 플랩을 볼 수 있다.

만든 방법

주경 세그먼트 18장은 평평한 윗변을 가진 육각형을 열 단위로 배열하고, 각 장을 동심 고리로 잘게 나눈 뒤 앞면 꼭짓점을 곡률반경 15.9m 구면 위로 옮겨 하나의 오목거울처럼 보이게 했다. 차양막은 모서리 사이가 안쪽으로 휜 육각 외곽선을 극좌표 격자로 채워 만들었고, 겹 사이 간격이 가장자리로 갈수록 벌어지도록 하고 사인 함수로 잔주름을 넣었다. 망원경부(OTE)는 빈 오브젝트 하나에 묶어 9도 위로 기울였고, 부경 지지대는 두 점을 잇는 원통으로 만들었다. 백플레인 트러스, 과학 기기 모듈, 우주선 버스 내부 구조, 차양막 전개 케이블과 덮개는 상자와 판으로 단순화하거나 생략했다.

build.py

이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 289줄.

# 제임스 웹 우주망원경(JWST) — 완전히 펼친 관측 상태.
# 주경: 평평한 윗변을 가진 육각 세그먼트 18장을 열(column) 배열로 놓고, 각 장의 앞면 꼭짓점을
# 곡률반경 15.9m 오목 구면 위로 옮겨 한 장의 큰 오목거울처럼 보이게 했다(bmesh 직접 생성).
# 차양막: 길쭉한 육각 연(kite) 모양 5겹. 가장자리는 장력 때문에 안쪽으로 오목하게 휘고,
# 겹 사이 간격은 중심에서 가장자리로 갈수록 벌어지게 극좌표 격자로 만들었다.
# 그 밖에 부경과 삼각 지지대, 후방 광학 배플, 검은 백플레인과 ISIM 상자, 전개 타워,
# 우주선 버스·태양전지판·고이득 안테나·모멘텀 플랩을 실제 치수(m)에 맞춰 단순화해 붙였다.
from atelier import *
SHOT = {"azimuth": -64, "elevation": 21, "lens": 45, "light": "studio", "background": "#161b2b"}
# ── 재질 ────────────────────────────────────────────────
GOLD = mat('Gold_Beryllium_Mirror', '#e8b852', rough=0.12, metal=1.0)
BLACK = mat('Black_Composite', '#24252b', rough=0.6, metal=0.1)
STRUT = mat('Graphite_Strut', '#3a3c42', rough=0.45, metal=0.3)
KAPTON_SUN = mat('Kapton_Sunshield_Purple', '#b9a2cf', rough=0.28, metal=0.75)
KAPTON = mat('Kapton_Sunshield_Silver', '#d6cddf', rough=0.18, metal=0.85)
MLI = mat('Gold_MLI_Blanket', '#c48d34', rough=0.42, metal=0.7)
SILVER = mat('Radiator_Silver', '#b9bdc4', rough=0.32, metal=0.75)
GREY = mat('Bus_Grey', '#7d828a', rough=0.45, metal=0.5)
SOLAR = mat('Solar_Cells', '#1f2c4e', rough=0.22, metal=0.4)
DISH = mat('Antenna_White', '#dcd8cc', rough=0.5, metal=0.0)
def tube(name, a, b, r, material, seg=10):
"""두 점 a→b 를 잇는 원통."""
a, b = Vector(a), Vector(b)
d = b - a
ob = cylinder(name, r=r, h=d.length, material=material, seg=seg, base=True)
ob.location = a
ob.rotation_euler = d.to_track_quat('Z', 'Y').to_euler()
return ob
# ── 차양막(Sunshield) 5겹 ────────────────────────────────
SS_HALF_L, SS_HALF_W, SS_TIP = 10.6, 7.08, 1.7
def ss_corners():
return [(SS_TIP, -SS_HALF_L), (SS_HALF_W, 0.0), (SS_TIP, SS_HALF_L),
(-SS_TIP, SS_HALF_L), (-SS_HALF_W, 0.0), (-SS_TIP, -SS_HALF_L)]
def ss_outline(per_edge=24, sag=0.32):
"""모서리 사이를 안쪽으로 오목하게 휜 외곽선(반시계). 짧은 끝변은 덜 휜다."""
cs = ss_corners()
pts = []
for i in range(6):
나머지 241줄 펼치기
p, q = Vector(cs[i]), Vector(cs[(i + 1) % 6])
e = q - p
n = Vector((-e.y, e.x)).normalized() # 반시계 외곽의 안쪽 법선
s_amt = sag * min(1.0, e.length / 8.0)
for k in range(per_edge):
s = k / per_edge
pts.append(p + e * s + n * (s_amt * math.sin(math.pi * s)))
return pts
def layer_z(i, t):
return i * (0.07 + 0.19 * t ** 1.3)
def sunshield_layer(i, scale, material):
outline = ss_outline()
rings = 20
bm = bmesh.new()
c = bm.verts.new((0, 0, layer_z(i, 0)))
grid = []
for k in range(1, rings + 1):
t = k / rings
row = []
for p in outline:
phi = math.atan2(p.y, p.x)
# 막이 팽팽히 당겨지며 생기는 잔주름(가장자리로 갈수록 커진다)
w = 0.03 * t * math.sin(11 * phi + 1.7 * i) * math.sin(math.pi * 2.5 * t)
row.append(bm.verts.new((p.x * t * scale, p.y * t * scale, layer_z(i, t) + w)))
grid.append(row)
n = len(outline)
for j in range(n):
bm.faces.new((c, grid[0][j], grid[0][(j + 1) % n]))
for k in range(rings - 1):
a, b = grid[k], grid[k + 1]
for j in range(n):
bm.faces.new((a[j], b[j], b[(j + 1) % n], a[(j + 1) % n]))
me = bpy.data.meshes.new(f'Sunshield_Layer_{i + 1}')
bm.to_mesh(me)
bm.free()
ob = link(bpy.data.objects.new(f'Sunshield_Layer_{i + 1}', me))
assign(ob, material)
solidify(ob, 0.02)
smooth(ob)
return ob
LAYER_SCALE = [1.0 - 0.02 * i for i in range(5)]
for i in range(5):
sunshield_layer(i, LAYER_SCALE[i], KAPTON_SUN if i < 2 else KAPTON)
# 모서리 스프레더 바: 각 겹의 모서리를 위아래로 꿴다
for ci, (cx, cy) in enumerate(ss_corners()):
pts = [(cx * LAYER_SCALE[i], cy * LAYER_SCALE[i], layer_z(i, 1.0)) for i in range(5)]
pts = [(pts[0][0], pts[0][1], -0.05)] + pts + [(pts[-1][0], pts[-1][1], pts[-1][2] + 0.06)]
curve_tube(f'Spreader_Bar_{ci + 1}', pts, radius=0.045, material=STRUT)
# 미드붐(좌우)과 앞뒤 팔레트 빔(맨 아래 겹 밑)
for sx in (-1, 1):
pts = []
for k in range(0, 11):
t = k / 10
pts.append((sx * SS_HALF_W * LAYER_SCALE[2] * t, 0.0, layer_z(2, t) + 0.04))
curve_tube(f'Mid_Boom_{"L" if sx < 0 else "R"}', pts, radius=0.06, material=STRUT)
for sy in (-1, 1):
curve_tube(f'Pallet_Beam_{"Fwd" if sy < 0 else "Aft"}', [(0, 0, -0.1), (0, sy * (SS_HALF_L - 0.1), -0.1)],
radius=0.08, material=STRUT)
# ── 우주선 버스(차양막 아래, 태양을 향하는 쪽) ───────────
BUS_Y = 0.4
bus = box('Spacecraft_Bus', (2.4, 2.4, 1.3), loc=(0, BUS_Y, -1.32), material=MLI, base=True)
bevel(bus, 0.04, 2)
box('Bus_Bottom_Deck', (2.5, 2.5, 0.08), loc=(0, BUS_Y, -1.38), material=GREY, base=True)
# 태양전지판: 버스 뒷면 아래 모서리에서 뒤로 비스듬히
sa_hinge = Vector((0, BUS_Y + 1.25, -1.2))
sa = box('Solar_Array', (2.5, 4.4, 0.06), loc=(0, 2.2, 0), material=SOLAR)
sa_frame = box('Solar_Array_Frame', (2.6, 4.5, 0.04), loc=(0, 2.2, 0.04), material=GREY)
sa_root = bpy.data.objects.new('Solar_Array_Hinge', None)
link(sa_root)
sa_root.location = sa_hinge
sa_root.rotation_euler = (math.radians(-14), 0, 0)
parent_to([sa, sa_frame], sa_root)
# 고이득 안테나: 버스 앞쪽 밑에 매달린 접시
hga_root = Vector((0.6, BUS_Y - 0.9, -1.38))
tube('HGA_Boom', hga_root, hga_root + Vector((0, -0.2, -0.9)), 0.05, GREY)
dish = lathe('HGA_Dish', [(0.0, 0.0), (0.2, 0.012), (0.4, 0.05), (0.58, 0.11), (0.6, 0.13)], material=DISH, seg=40)
solidify(dish, 0.02)
dish.location = hga_root + Vector((0, -0.2, -0.88))
dish.rotation_euler = (math.radians(200), 0, 0)
# 모멘텀 트림 플랩: 뒤쪽 끝
flap_pts = [(-1.5, 0), (1.5, 0), (0.9, 1.7), (-0.9, 1.7)]
flap = prism('Momentum_Trim_Flap', flap_pts, 0.04, material=KAPTON)
flap.location = (0, SS_HALF_L, 0.03)
flap.rotation_euler = (math.radians(32), 0, 0)
tube('Flap_Hinge', (-SS_TIP, SS_HALF_L, 0.05), (SS_TIP, SS_HALF_L, 0.05), 0.05, STRUT)
# 버스 옆면 방열판과 별 추적기
for sx in (-1, 1):
box(f'Bus_Radiator_{"L" if sx < 0 else "R"}', (0.04, 1.9, 0.9), loc=(sx * 1.22, BUS_Y, -1.12), material=SILVER, base=True)
for i, ang in enumerate((-30, 30)):
st = cylinder(f'Star_Tracker_{i + 1}', r=0.13, r2=0.17, h=0.4, material=GREY, seg=20, base=True)
st.location = (ang / 40, BUS_Y - 1.15, -1.1)
st.rotation_euler = (math.radians(120), 0, math.radians(ang))
# ── 광학 망원경 요소(OTE) ─ 로컬 좌표: 거울 정점 원점, -Y 가 관측 방향 ──
ote = bpy.data.objects.new('Optical_Telescope_Element', None)
link(ote)
OTE_TILT = math.radians(-9)
ote.location = (0, -0.9, 4.55)
ote.rotation_euler = (OTE_TILT, 0, 0)
ote_parts = []
RC = 15.88 # 주경 곡률반경
R0 = 1.32 / math.sqrt(3) # 세그먼트 피치(외접반지름)
RG = R0 - 0.012 # 틈을 둔 실제 외접반지름
H = math.sqrt(3) * R0
SEG_T = 0.06
def sag(u, v):
return -(u * u + v * v) / (2 * RC)
def seg_centers():
out = []
for c in range(-2, 3):
u = c * 1.5 * R0
if c == 0:
ks = [k * H for k in (-2, -1, 1, 2)]
elif abs(c) == 2:
ks = [k * H for k in (-1, 0, 1)]
else:
ks = [(k + 0.5) * H for k in (-2, -1, 0, 1)]
out += [(u, v) for v in ks]
return out
def hex_ring(cu, cv, r, sub=2):
pts = []
for i in range(6):
a0, a1 = math.radians(60 * i), math.radians(60 * (i + 1))
p0 = Vector((math.cos(a0), math.sin(a0))) * r
p1 = Vector((math.cos(a1), math.sin(a1))) * r
for s in range(sub):
p = p0.lerp(p1, s / sub)
pts.append((cu + p.x, cv + p.y))
return pts
def build_segments(name, radius, y_off, depth, material, rings=3, smooth_front=True):
bm = bmesh.new()
for cu, cv in seg_centers():
c = bm.verts.new((cu, sag(cu, cv) + y_off, cv))
front = []
for k in range(1, rings + 1):
ring = hex_ring(cu, cv, radius * k / rings, sub=2 * k)
front.append([bm.verts.new((u, sag(u, v) + y_off, v)) for u, v in ring])
outer = front[-1]
back = [bm.verts.new((v.co.x, v.co.y + depth, v.co.z)) for v in outer]
bc = bm.verts.new((cu, sag(cu, cv) + y_off + depth, cv))
n0 = len(front[0])
for j in range(n0):
f = bm.faces.new((c, front[0][(j + 1) % n0], front[0][j]))
f.smooth = smooth_front
for k in range(rings - 1):
a, b = front[k], front[k + 1]
na, nb = len(a), len(b)
# 바깥 고리는 꼭짓점 수가 다르다: 육각 변마다 삼각형 띠로 잇는다
sa_, sb_ = na // 6, nb // 6
for e in range(6):
ia = [e * sa_ + s for s in range(sa_ + 1)]
ib = [e * sb_ + s for s in range(sb_ + 1)]
i, j = 0, 0
while i < sa_ or j < sb_:
if j < sb_ and (i >= sa_ or (j + 1) / sb_ <= (i + 1) / sa_):
f = bm.faces.new((a[ia[i] % na], b[ib[j + 1] % nb], b[ib[j] % nb]))
j += 1
else:
f = bm.faces.new((a[ia[i] % na], a[ia[i + 1] % na], b[ib[j] % nb]))
i += 1
f.smooth = smooth_front
no = len(outer)
for j in range(no):
bm.faces.new((outer[j], outer[(j + 1) % no], back[(j + 1) % no], back[j]))
bm.faces.new((bc, back[j], back[(j + 1) % no]))
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = link(bpy.data.objects.new(name, me))
return assign(ob, material)
ote_parts.append(build_segments('Primary_Mirror_18_Segments', RG, 0.0, SEG_T, GOLD, rings=3))
# 세그먼트 뒤의 검은 백플레인(세그먼트보다 살짝 큰 육각 블록이 이어져 한 판처럼 보인다)
ote_parts.append(build_segments('Primary_Backplane', R0 + 0.004, SEG_T + 0.005, 0.38, BLACK, rings=1, smooth_front=False))
# 중앙 구멍 속 후방 광학 배플(AOS)
aos = cylinder('Aft_Optics_Baffle', r=0.42, r2=0.34, h=1.25, material=BLACK, seg=24, base=True)
aos.location = (0, 0.35, 0)
aos.rotation_euler = (math.radians(90), 0, 0)
ote_parts.append(aos)
aos_lip = torus('Aft_Optics_Lip', R=0.35, r=0.03, material=STRUT, seg=32, rseg=8)
aos_lip.location = (0, -0.9, 0)
aos_lip.rotation_euler = (math.radians(90), 0, 0)
ote_parts.append(aos_lip)
# 부경과 삼각 지지대
SM_Y = -7.17
sm = cylinder('Secondary_Mirror', r=0.37, h=0.05, material=GOLD, seg=36, base=True)
sm.location = (0, SM_Y + 0.05, 0)
sm.rotation_euler = (math.radians(90), 0, 0)
sm_cell = cylinder('Secondary_Mirror_Cell', r=0.42, r2=0.3, h=0.35, material=BLACK, seg=36, base=True)
sm_cell.location = (0, SM_Y, 0)
sm_cell.rotation_euler = (math.radians(-90), 0, 0)
ote_parts += [sm, sm_cell]
top_v = 2 * H + R0 * math.sqrt(3) / 2
strut_feet = [(0.0, sag(0, top_v) + 0.1, top_v - 0.05),
(2.55, sag(2.55, -1.75) + 0.1, -1.75), (-2.55, sag(2.55, -1.75) + 0.1, -1.75)]
strut_heads = [(0.0, SM_Y - 0.3, 0.38), (0.33, SM_Y - 0.3, -0.2), (-0.33, SM_Y - 0.3, -0.2)]
for i, (a, b) in enumerate(zip(strut_feet, strut_heads)):
ote_parts.append(tube(f'SM_Support_Strut_{i + 1}', a, b, 0.055, STRUT))
ote_parts.append(sphere(f'SM_Strut_Hinge_{i + 1}', 0.1, loc=a, material=STRUT, seg=12, rings=8))
# 백플레인 뒤: ISIM 상자, 방열판, 지지 트러스
isim = box('ISIM_Enclosure', (2.0, 1.6, 2.6), loc=(0, 1.25, -1.0), material=BLACK)
bevel(isim, 0.03, 1)
rad = box('ISIM_Radiator', (2.4, 0.08, 3.2), loc=(0, 2.1, -0.6), material=SILVER)
ote_parts += [isim, rad]
for sx in (-1, 1):
ote_parts.append(tube(f'Backplane_Support_Truss_{sx}', (sx * 1.9, 0.45, -2.6), (sx * 0.9, 1.9, -2.2), 0.05, STRUT))
ote_parts.append(tube(f'Backplane_Support_Truss_Up_{sx}', (sx * 1.9, 0.45, 2.0), (sx * 0.9, 2.05, 0.9), 0.05, STRUT))
parent_to(ote_parts, ote)
# 전개 타워: ISIM 아래에서 버스 위까지
bpy.context.view_layer.update()
tower_top = ote.matrix_world @ Vector((0, 1.3, -2.25))
tube('Deployable_Tower', (tower_top.x, tower_top.y, -0.05), (tower_top.x, tower_top.y, tower_top.z + 0.05), 0.28, MLI, seg=20)

다른 모형