
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
아토미움은 1958년 브뤼셀 만국박람회(Expo 58)를 위해 벨기에 브뤼셀 헤이젤 고원에 세워졌다. 공학자 앙드레 바테르케인이 구상하고 건축가 앙드레·장 폴라크 형제가 설계했으며, 원자력 시대에 대한 낙관과 과학에 대한 신뢰를 형상으로 보여 주려는 의도였다. 박람회가 끝난 뒤에도 철거되지 않고 남아 브뤼셀을 대표하는 건축물이 되었다.
구조는 철 결정의 체심입방 단위세포를 그대로 확대한 것이다. 지름 18m의 구 9개가 정육면체의 꼭짓점 8곳과 중심에 놓이고, 모서리와 대각선을 따라 관으로 이어진다. 정육면체는 꼭짓점 하나를 바닥에 대고 서 있어 몸체 대각선이 수직이 되며, 전체 높이는 102m이다. 중심을 관통하는 수직 관에는 엘리베이터가, 비스듬한 관에는 에스컬레이터와 계단이 들어 있고, 맨 위 구에는 전망대가 있다. 2004년부터 2006년까지의 보수 공사에서 원래의 알루미늄 외피가 스테인리스 강판으로 교체되었다.
이 모델은 실제 비례에 맞춘 구 9개와 연결관 20개, 아래쪽 세 구를 받치는 A자형 이각 지지대 3쌍, 바닥 구 아래의 엘리베이터 기둥과 입구 건물을 표현했다. 구 표면에는 삼각형 스테인리스 패널의 이음매와 둥근 창을, 맨 위 구에는 전망대 유리띠를 두었다.
만든 방법
정육면체 모서리 41m를 기준으로 꼭짓점 좌표를 계산해 구 중심 9곳을 정하고, 모서리 길이와 같은 두 꼭짓점 쌍을 찾아 모서리 관 12개를, 중심에서 각 꼭짓점으로 대각선 관 8개를 자동으로 배치했다. 구는 매끈한 UV 구 위에 3단계 분할 icosphere 를 bmesh 와이어프레임으로 바꾼 이음매 망을 얹어 삼각 패널을 표현했으며, 실제 패널 배치를 따른 것이 아니라 측지 분할로 단순화했다. 둥근 창은 적도 부근 패널 중심에 짧은 원기둥을 박아 넣었다. 관은 일정 간격의 이음 띠를 넣은 회전체(lathe)이고, 같은 길이의 관·고리·구 부품은 linked 복제로 메시를 공유한다. 지지대는 아래위 단면이 다른 사각 기둥을 두 점 사이에 세워 만들었고, 내부 공간과 2006년 신축 입구 건물의 실제 형태는 원통형 파빌리온으로 단순화했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 233줄.
# 아토미움 (Atomium, 브뤼셀, 1958)# 철 결정의 체심입방 단위세포를 1650억 배로 키운 구조물. 지름 18m 구 9개를# 정육면체 모서리 12개 + 중심-꼭짓점 대각선 8개, 모두 20개의 관으로 잇고,# 정육면체를 꼭짓점 하나로 세워 몸체 대각선이 수직이 되게 했다.# 구는 매끈한 UV 구 위에 측지 삼각 분할(icosphere)의 이음매를 bmesh 와이어프레임으로 얹어# 2006년 리노베이션의 스테인리스 삼각 패널을 표현했고, 관·구·패널은 linked 복제로 메시를 공유한다.# 아래쪽 세 구를 받치는 A자형 이각 지지대 3쌍, 바닥 구 아래 엘리베이터 기둥과 입구 파빌리온까지 만들었다.from atelier import *SHOT = {"azimuth": -78, "elevation": 8, "lens": 50, "light": "day", "background": "#34404f"}# ---- 치수 (미터) ----R_S = 9.0 # 구 반지름 (지름 18m)R_T = 1.65 # 관 반지름 (지름 3.3m)EDGE = 41.0 # 정육면체 모서리(구 중심 간)HALF_D = EDGE * math.sqrt(3) / 2 # 중심-꼭짓점 거리RHO = EDGE * math.sqrt(2 / 3) # 고리 꼭짓점의 수평 반지름Z_BOT = 102.0 - R_S - 2 * HALF_D # 맨 아래 구 중심 높이 (꼭대기 102m)Z_C = Z_BOT + HALF_D # 가운데 구 중심# ---- 재질 ----M_STEEL = mat('StainlessPanel', '#cdd2d7', rough=0.2, metal=1.0)M_SEAM = mat('PanelSeam', '#5f666e', rough=0.45, metal=1.0)M_TUBE = mat('TubeSteel', '#b9bfc5', rough=0.24, metal=1.0)M_COLLAR = mat('CollarSteel', '#9da4ab', rough=0.3, metal=1.0)M_WIN = mat('PortholeGlass', '#1d2832', rough=0.06, metal=0.2)M_BAND = mat('PanoramaGlass', '#27384a', rough=0.05, metal=0.3)M_LEG = mat('BipodSteel', '#8f969d', rough=0.42, metal=0.7)M_CONC = mat('Concrete', '#a8a397', rough=0.88)M_PAV = mat('PavilionGlass', '#3b4c5a', rough=0.08, metal=0.3)M_PAVF = mat('PavilionFrame', '#5d636a', rough=0.5, metal=0.6)Z = Vector((0, 0, 1))def orient(ob, a, b): """Z 축 기준으로 만든 오브젝트를 a→b 방향으로 세운다.""" d = (Vector(b) - Vector(a)).normalized() ob.rotation_mode = 'QUATERNION' ob.rotation_quaternion = Z.rotation_difference(d) ob.location = Vector(a) return ob# ---- 꼭짓점(구 중심) 9개 ----C = Vector((0, 0, Z_C))TOP = C + Vector((0, 0, HALF_D))BOT = C - Vector((0, 0, HALF_D))나머지 185줄 펼치기접기
UP = [C + Vector((RHO * math.cos(math.radians(a)), RHO * math.sin(math.radians(a)), HALF_D / 3)) for a in (30, 150, 270)]LO = [C + Vector((RHO * math.cos(math.radians(a)), RHO * math.sin(math.radians(a)), -HALF_D / 3)) for a in (90, 210, 330)]NODES = {'Top': TOP, 'Center': C, 'Bottom': BOT}for i, p in enumerate(UP): NODES['Upper%d' % (i + 1)] = pfor i, p in enumerate(LO): NODES['Lower%d' % (i + 1)] = pEDGES = [('Top', 'Upper%d' % (i + 1)) for i in range(3)] + [('Bottom', 'Lower%d' % (i + 1)) for i in range(3)]for i, u in enumerate(UP): for j, l in enumerate(LO): if abs((u - l).length - EDGE) < 0.5: EDGES.append(('Upper%d' % (i + 1), 'Lower%d' % (j + 1)))DIAGS = [('Center', k) for k in NODES if k != 'Center']# ---- 구: 매끈한 몸체 + 삼각 패널 이음매 + 둥근 창 ----def make_sphere_parts(): body = sphere('Sphere_Body', r=R_S, seg=72, rings=36, material=M_STEEL) bm = bmesh.new() bmesh.ops.create_icosphere(bm, subdivisions=3, radius=R_S + 0.03) centers = [(f.calc_center_median().copy()) for f in bm.faces] bmesh.ops.wireframe(bm, faces=bm.faces[:], thickness=0.13, offset=0.0, use_replace=True, use_even_offset=True, use_relative_offset=False, use_boundary=True) seams = _bm_obj('Sphere_PanelSeams', bm, M_SEAM) # 둥근 창: 적도 위아래 띠의 패널 중앙 bm = bmesh.new() for c in centers: n = c.normalized() lat = math.degrees(math.asin(max(-1, min(1, n.z)))) lon = math.degrees(math.atan2(n.y, n.x)) if -14 < lat < 10 and int((lon + 180) // 15) % 2 == 0: _disc(bm, n, R_S - 0.05, R_S + 0.06, 0.38, 16) wins = _bm_obj('Sphere_Portholes', bm, M_WIN) return body, seams, winsdef _bm_obj(name, bm, material): me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) link(ob) return assign(ob, material)def _disc(bm, n, r0, r1, rad, seg): """방향 n 을 축으로 하는 짧은 원기둥(창).""" q = Z.rotation_difference(n) ring0, ring1 = [], [] for k in range(seg): a = 2 * math.pi * k / seg v = q @ Vector((rad * math.cos(a), rad * math.sin(a), 0)) ring0.append(bm.verts.new(n * r0 + v)) ring1.append(bm.verts.new(n * r1 + v)) bm.faces.new(list(reversed(ring0))) bm.faces.new(ring1) for k in range(seg): bm.faces.new((ring0[k], ring0[(k + 1) % seg], ring1[(k + 1) % seg], ring1[k]))body, seams, wins = make_sphere_parts()used = set()def _inst(ob): if ob.name in used: return duplicate(ob) used.add(ob.name) return obfor name, p in NODES.items(): parts = [(body, 'Body'), (seams, 'PanelSeams')] if name != 'Top': # 맨 위 구는 둥근 창 대신 전망 유리띠 parts.append((wins, 'Portholes')) for src, suf in parts: o = _inst(src) o.location = p o['label'] = 'Sphere_%s_%s' % (name, suf)for ob in list(bpy.context.scene.objects): if 'label' in ob: ob.name = ob['label'] del ob['label']# 맨 위 구의 전망대 유리띠 (구면을 따라 도는 띠)band_prof = []for k in range(9): lat = math.radians(-6 + 20 * k / 8) band_prof.append(((R_S + 0.015) * math.cos(lat), (R_S + 0.015) * math.sin(lat)))lathe('Sphere_Top_PanoramaWindows', band_prof, loc=TOP, material=M_BAND, seg=96)# ---- 관(20개) + 이음 고리 ----def tube_profile(length, pitch=4.0): """관 단면 프로필: 일정 간격마다 살짝 도드라진 패널 이음 띠.""" prof = [(0.0, 0.0), (R_T, 0.0)] n = max(1, int(length / pitch)) step = length / n for k in range(1, n): z = k * step prof += [(R_T, z - 0.16), (R_T, z - 0.1), (R_T + 0.025, z - 0.07), (R_T + 0.025, z + 0.07), (R_T, z + 0.1), (R_T, z + 0.16)] prof += [(R_T, length), (0.0, length)] return profdef tube_between(name, a, b, mesh_cache): d = (b - a) L = d.length u = d.normalized() inset = math.sqrt(R_S ** 2 - R_T ** 2) - 0.25 p0 = a + u * inset length = L - 2 * inset key = round(length, 2) if key not in mesh_cache: t = lathe('Tube', tube_profile(length), material=M_TUBE, seg=40) mesh_cache[key] = t ob = t else: ob = duplicate(mesh_cache[key]) ob.name = 'Tube_' + name orient(ob, p0, b) # 구와 만나는 고리 (양 끝) for end, (pa, sgn) in enumerate(((a, 1), (b, -1))): if 'collar' not in mesh_cache: cl = cylinder('Collar', r=R_T + 0.35, h=1.1, seg=40, material=M_COLLAR) smooth(cl) mesh_cache['collar'] = cl co = cl else: co = duplicate(mesh_cache['collar']) co.name = 'TubeCollar_%s_%d' % (name, end) start = pa + u * sgn * (math.sqrt(R_S ** 2 - (R_T + 0.35) ** 2) - 0.2) orient(co, start, start + u * sgn) return obcache = {}for a, b in EDGES + DIAGS: tube_between('%s-%s' % (a, b), NODES[a], NODES[b], cache)# ---- 바닥 구 아래 엘리베이터 기둥 ----shaft_top = BOT.z - math.sqrt(R_S ** 2 - R_T ** 2) + 0.25shaft = lathe('ElevatorShaft', tube_profile(shaft_top), material=M_TUBE, seg=40)# ---- 이각 지지대 3쌍 (아래쪽 세 구를 받침) ----def leg(name, foot, top, w0=3.0, d0=2.2, w1=1.9, d1=1.5): bm = bmesh.new() bot = [bm.verts.new(v) for v in ((-w0 / 2, -d0 / 2, 0), (w0 / 2, -d0 / 2, 0), (w0 / 2, d0 / 2, 0), (-w0 / 2, d0 / 2, 0))] L = (top - foot).length tp = [bm.verts.new(v) for v in ((-w1 / 2, -d1 / 2, L), (w1 / 2, -d1 / 2, L), (w1 / 2, d1 / 2, L), (-w1 / 2, d1 / 2, L))] bm.faces.new(list(reversed(bot))) bm.faces.new(tp) for k in range(4): bm.faces.new((bot[k], bot[(k + 1) % 4], tp[(k + 1) % 4], tp[k])) ob = _bm_obj(name, bm, M_LEG) orient(ob, foot, top) bevel(ob, 0.12, 2) return obfor i, p in enumerate(LO): radial = Vector((p.x, p.y, 0)).normalized() tang = Vector((-radial.y, radial.x, 0)) apex = Vector((p.x, p.y, 0)) + radial * 1.0 + Vector((0, 0, p.z - R_S + 0.6)) for s in (-1, 1): foot = Vector((p.x, p.y, 0)) + radial * 5.5 + tang * s * 11.0 leg('Bipod%d_Leg%s' % (i + 1, 'L' if s < 0 else 'R'), foot, apex) pad = box('Bipod%d_Footing%s' % (i + 1, 'L' if s < 0 else 'R'), size=(4.2, 4.2, 0.8), loc=(foot.x, foot.y, 0), base=True, material=M_CONC) pad.rotation_euler.z = math.atan2(radial.y, radial.x) # 지지대 꼭대기와 구 사이 받침 sad = cylinder('Bipod%d_Saddle' % (i + 1), r=2.4, h=2.4, r2=1.6, seg=24, loc=(apex.x, apex.y, apex.z - 1.6), material=M_LEG) smooth(sad)# ---- 입구 파빌리온 (바닥 구 아래) ----pav_r = 7.5cylinder('Pavilion_Plinth', r=pav_r + 1.0, h=0.6, seg=48, material=M_CONC)cylinder('Pavilion_Glass', r=pav_r, h=4.2, seg=48, loc=(0, 0, 0.6), material=M_PAV)cylinder('Pavilion_Roof', r=pav_r + 0.6, h=0.7, seg=48, loc=(0, 0, 4.8), material=M_PAVF)mull = box('Pavilion_Mullion', size=(0.18, 0.25, 4.2), loc=(pav_r + 0.05, 0, 0.6), base=True, material=M_PAVF)ring_of(mull, 24, pav_r + 0.05)






