
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
제임스 웹 우주망원경(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.7def 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 ptsdef 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 obLAYER_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.4bus = 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_hingesa_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) * R0SEG_T = 0.06def 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 outdef 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 ptsdef 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.17sm = 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) / 2strut_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)






