
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
판테온은 로마 시내 로톤다 광장에 있는 고대 로마의 신전이다. 기원전 1세기 마르쿠스 아그리파가 처음 세운 건물이 불탄 뒤, 하드리아누스 황제 시기인 서기 125년 무렵 지금의 모습으로 다시 지어졌다. 정면 프리즈에는 처음 건립자를 기리는 명문 'M·AGRIPPA·L·F·COS·TERTIVM·FECIT'가 남아 있다. 609년 성당으로 봉헌된 덕분에 오늘날까지 거의 온전하게 쓰이고 있다.
건물은 원통형 본당(로툰다), 박공을 얹은 직사각형 포르티코, 그리고 둘을 잇는 중간 블록으로 이루어진다. 로툰다 내부는 지름과 바닥에서 천장 꼭대기까지의 높이가 모두 약 43.3m여서 지름이 같은 구 하나가 들어맞는다. 철근 없는 콘크리트 돔으로는 지금도 세계에서 가장 크며, 꼭대기에는 지름 약 8m의 오쿨루스가 뚫려 있어 유일한 채광창 구실을 한다.
이 모델은 회색 이집트산 화강암 기둥 8개를 앞줄에, 분홍빛 화강암 기둥을 안쪽 두 줄에 4개씩 세운 포르티코 배치, 돔 아래쪽의 계단식 고리, 벽돌 벽에 드러난 하중분산 아치, 세 단의 코니스, 청동 문과 양옆 벽감을 표현했다. 오쿨루스 아래로는 반구형 안쪽 천장과 격자 천장(코퍼)이 내려다보인다.
만든 방법
로툰다 벽, 코니스, 돔의 계단식 고리와 납판 돔, 내부 반구 천장은 모두 단면 프로필을 Z 축으로 돌린 회전체(lathe)로 만들었다. 납판 돔은 계단 고리 끝과 오쿨루스 테두리를 지나는 구면 호로 계산해 실제처럼 낮고 넓은 실루엣을 냈다. 코린트식 기둥은 엔타시스를 준 회전체 기둥 몸통에 바깥으로 말린 아칸서스 잎 16장과 볼류트를 bmesh 로 붙인 뒤, 회색·분홍 두 종류의 메시를 16개 위치에 공유 복제했다. 엔타블러처와 박공은 상자와 삼각 단면 돌출로 쌓고, 지붕 리브·모딜리온·벽돌 아치·내부 코퍼는 수학적 배열로 배치했다. 벽돌의 질감, 기둥머리 조각의 세부, 박공의 옛 청동 장식 흔적과 주변 광장은 생략했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 334줄.
# 판테온 (로마, 서기 125년경) — Blender Python 코드로만 만든 모델.# 원형 본당(로툰다)과 얕은 돔은 단면 프로필을 Z 축으로 돌린 회전체(lathe)로 만들고,# 돔 아래쪽의 계단식 고리, 지름 약 8m 의 오쿨루스, 안쪽 반구 천장과 격자 천장(코퍼)까지 넣었다.# 정면 포르티코는 화강암 기둥 16개(앞줄 8개 + 안쪽 두 줄 4개씩)를 회전체+아칸서스 잎 배열로 만든# 기둥 두 종류의 메시 공유 복제로 세우고, 엔타블러처·박공(페디먼트)·납 지붕은 상자와 돌출 단면으로 쌓았다.# 실제 치수(내부 지름·높이 43.3m, 포르티코 폭 약 33m)를 기준으로 했고, 벽돌 아치 등은 단순화했다.from atelier import *SHOT = {"azimuth": -58, "elevation": 13, "lens": 45, "light": "golden", "background": "#27303b"}# ── 재질 ──────────────────────────────────────────────M_BRICK = mat('Roman_Brick_Stucco', '#877565', 0.85)M_ARCH = mat('Relieving_Arch_Brick', '#665243', 0.9)M_MARBLE = mat('Weathered_Marble', '#d0c6b2', 0.55)M_TRAV = mat('Travertine_Steps', '#c9bc9f', 0.75)M_GRAN_G = mat('Egyptian_Grey_Granite', '#86827d', 0.35)M_GRAN_P = mat('Pink_Aswan_Granite', '#a07f74', 0.35)M_LEAD = mat('Lead_Dome_Sheet', '#8a8c8a', 0.6, 0.1)M_STUCCO = mat('Dome_Step_Rings', '#b4ada1', 0.8)M_ROOF = mat('Portico_Roof_Lead', '#5c6265', 0.55, 0.3)M_BRONZE = mat('Bronze_Doors', '#5b4630', 0.4, 0.85)M_INSCR = mat('Inscription_Bronze', '#4a3a28', 0.45, 0.7)M_INNER = mat('Interior_Coffer_Concrete', '#bfb5a3', 0.8)M_NICHE = mat('Niche_Shadow_Stone', '#6b5f52', 0.9)TAN = math.tan(math.radians(23.0)) # 박공 경사# ── 로툰다 치수 ───────────────────────────────────────R_OUT = 28.5 # 바깥 벽 반지름R_IN = 21.65 # 안쪽 반지름 (지름 43.3m)DRUM_TOP = 31.0seg = 144drum = lathe('Rotunda_Drum_Wall', [(R_IN, 0.0), (29.0, 0.0), (29.0, 1.3), (R_OUT, 1.5), (R_OUT, DRUM_TOP), (27.8, DRUM_TOP)], material=M_BRICK, seg=seg, shade_smooth=True)# 바깥 코니스 고리 3단(대리석)def cornice_ring(name, h, proj=0.75): p = [(28.3, h - 0.95), (R_OUT + 0.15, h - 0.95), (R_OUT + 0.2, h - 0.7), (R_OUT + proj * 0.6, h - 0.5), (R_OUT + proj, h - 0.2), (R_OUT + proj, h), (28.3, h)] return lathe(name, p, material=M_MARBLE, seg=seg, shade_smooth=False)cornice_ring('Rotunda_Cornice_Lower', 19.3)cornice_ring('Rotunda_Cornice_Middle', 25.5)cornice_ring('Rotunda_Cornice_Top', DRUM_TOP + 0.05, 0.85)# 돔 하부 계단식 고리 (7단)steps = [(27.8, DRUM_TOP)]나머지 286줄 펼치기접기
r, z = 27.8, DRUM_TOPfor i in range(7): z += 0.9 steps.append((r, z)) r -= 0.9 steps.append((r, z))lathe('Dome_Step_Rings', steps, material=M_STUCCO, seg=seg, shade_smooth=False)R_CAP0, Z_CAP0 = r, z # (21.5, 37.3)# 납판 돔 — 두 점을 지나는 구면 호R_OC, Z_OC = 4.6, 44.3c = ((R_OC ** 2 + Z_OC ** 2) - (R_CAP0 ** 2 + Z_CAP0 ** 2)) / (2 * (Z_OC - Z_CAP0))rho = math.hypot(R_OC, Z_OC - c)a0 = math.atan2(Z_CAP0 - c, R_CAP0)a1 = math.atan2(Z_OC - c, R_OC)cap = []for i in range(25): a = a0 + (a1 - a0) * i / 24 cap.append((rho * math.cos(a), c + rho * math.sin(a)))cap += [(R_OC, Z_OC + 0.25)]lathe('Dome_Lead_Shell', cap, material=M_LEAD, seg=seg)# 오쿨루스 청동 테torus('Oculus_Bronze_Rim', R=R_OC - 0.15, r=0.28, loc=(0, 0, Z_OC + 0.25), material=M_BRONZE, seg=64, rseg=10)# 내부: 원통 벽 + 반구 천장inner = [(R_OC + 0.1, Z_OC + 0.25), (4.1, Z_OC + 0.25), (4.1, 42.9)]for i in range(1, 31): a = math.radians(79.1 - 79.1 * i / 30) inner.append((R_IN * math.cos(a), R_IN + R_IN * math.sin(a)))inner += [(R_IN, 0.3)]lathe('Rotunda_Interior_Surface', inner, material=M_INNER, seg=seg)cylinder('Rotunda_Floor', r=R_IN + 0.05, h=0.3, material=M_MARBLE, seg=seg)# 내부 격자 천장(코퍼) — 5단 × 28열, 오쿨루스로 내려다보인다coffer = box('Coffer_Rib', size=(0.5, 2.2, 0.35), material=M_INNER)rows = [(25, 2.9), (37, 2.6), (49, 2.3), (61, 1.9), (71, 1.5)]cof = []for k, (deg, ln) in enumerate(rows): el = math.radians(deg) for j in range(28): az = 2 * math.pi * (j + 0.5) / 28 rr = (R_IN - 0.15) * math.cos(el) zz = R_IN + (R_IN - 0.15) * math.sin(el) o = duplicate(coffer, loc=(rr * math.cos(az), rr * math.sin(az), zz), rot=(0, math.pi / 2 - el, az), scale=(1, ln / 2.2, 1), name=f'Coffer_{k}_{j}') cof.append(o)bpy.data.objects.remove(coffer, do_unlink=True)# ── 중간 블록(아반코르포) ─────────────────────────────BX = 17.4B_FRONT = -33.5B_BACK = -21.0B_TOP = 25.5box('Intermediate_Block', size=(2 * BX, B_BACK - B_FRONT, B_TOP), loc=(0, (B_FRONT + B_BACK) / 2, 0), material=M_BRICK, base=True)# 블록 코니스(대리석) 두 단for nm, h in (('Block_Cornice_Lower', 19.3), ('Block_Cornice_Upper', B_TOP + 0.05)): box(nm, size=(2 * BX + 1.4, B_BACK - B_FRONT + 0.7, 0.95), loc=(0, (B_FRONT + B_BACK) / 2 - 0.35, h - 0.95), material=M_MARBLE, base=True)# 블록 박공apex_b = B_TOP + 0.05 + (BX + 0.7) * TANBPW = BX + 0.7extrude_profile('Block_Pediment', [(-BPW, B_TOP), (BPW, B_TOP), (0, B_TOP + BPW * TAN)], depth=8.0, loc=(0, B_FRONT + 4.0, 0.0), material=M_BRICK)def gable_roof(prefix, half_w, base_z, y0, y1, thick=0.6): """경사 지붕 납판 두 장 + 앞쪽 경사 코니스(대리석). 처마선은 (±half_w, base_z).""" a = math.radians(23.0) L = half_w / math.cos(a) + 0.4 mz = base_z + half_w * TAN / 2 out = [] for s in (-1, 1): side = 'L' if s < 0 else 'R' for nm, off, size, ya, yb, m in (('Roof', thick / 2, thick, y0, y1, M_ROOF), ('Raking_Cornice', thick / 2 - 0.3, thick + 0.5, y0 - 0.15, y0 + 1.0, M_MARBLE)): cx = s * (half_w / 2 + math.sin(a) * off) cz = mz + math.cos(a) * off out.append(box(f'{prefix}_{nm}_{side}', size=(L if nm == 'Roof' else L - 0.3, yb - ya, size), loc=(cx, (ya + yb) / 2, cz), rot=(0, s * a, 0), material=m)) # 지붕 덮개 기와 이음(리브) off = thick + 0.06 cx = s * (half_w / 2 + math.sin(a) * off) cz = mz + math.cos(a) * off n = int((y1 - y0 - 1.2) / 0.9) rib = box(f'{prefix}_Roof_Rib_{side}_0', size=(L - 0.2, 0.2, 0.16), loc=(cx, y0 + 1.2, cz), rot=(0, s * a, 0), material=M_ROOF) for i in range(1, n): duplicate(rib, loc=(cx, y0 + 1.2 + i * 0.9, cz), name=f'{prefix}_Roof_Rib_{side}_{i}') return outgable_roof('Block', BX + 0.7, B_TOP + 0.05, B_FRONT - 0.35, B_FRONT + 6.5)# ── 포르티코 ─────────────────────────────────────────P_FRONT = -48.9 # 기단 앞면STYLO = 1.5COL_H = 14.2ENT_Z = STYLO + COL_H # 15.7ROW_Y = (-47.6, -43.1, -38.6)XS = [(i - 3.5) * 4.45 for i in range(8)]# 기단 + 계단 (단면을 X 방향으로 돌출)prof = [(B_FRONT + 0.5, 0), (P_FRONT - 2.0, 0)]for k in range(5): y = P_FRONT - 2.0 + 0.4 * k prof += [(y, 0.3 * (k + 1)), (y + 0.4, 0.3 * (k + 1))]prof[-1] = (P_FRONT, STYLO)prof += [(B_FRONT + 0.5, STYLO)]# 마지막 계단 단을 기단 윗면과 맞춘다extrude_profile('Portico_Podium_Steps', prof, depth=2 * 17.0, axis='X', loc=(0, 0, 0), material=M_TRAV)# 기둥 원형 만들기def make_column(name, shaft_mat): parts = [] parts.append(box(name + '_Plinth', size=(1.95, 1.95, 0.3), material=M_MARBLE, base=True)) base_p = [(0, 0.3), (0.93, 0.3), (0.96, 0.4), (0.92, 0.5), (0.82, 0.54), (0.80, 0.6), (0.87, 0.66), (0.85, 0.74), (0.77, 0.78)] parts.append(lathe(name + '_Base', base_p + [(0, 0.78)], material=M_MARBLE, seg=32)) sh = [] for i in range(10): t = i / 9 sh.append((0.76 - 0.12 * t ** 1.6, 0.76 + (12.1 - 0.76) * t)) parts.append(lathe(name + '_Shaft', [(0, 0.76)] + sh + [(0, 12.1)], material=shaft_mat, seg=32)) bell = [(0, 12.05), (0.70, 12.05), (0.71, 12.22), (0.64, 12.25), (0.65, 12.6), (0.70, 13.1), (0.78, 13.55), (0.86, 13.8), (0, 13.8)] parts.append(lathe(name + '_Capital_Bell', bell, material=M_MARBLE, seg=32)) parts.append(box(name + '_Abacus', size=(2.0, 2.0, 0.42), loc=(0, 0, 13.78), material=M_MARBLE, base=True)) # 아칸서스 잎: 바깥으로 말린 얇은 판 def leaf(nm, h, w): bm = bmesh.new() n = 6 fr, bk = [], [] for i in range(n): t = i / (n - 1) ww = w * (1 - 0.55 * t) * (0.7 + 0.3 * math.sin(math.pi * min(1, t * 1.6))) x = 0.32 * t ** 2 zz = h * t fr.append((bm.verts.new((x + 0.05, -ww / 2, zz)), bm.verts.new((x + 0.05, ww / 2, zz)))) bk.append((bm.verts.new((x - 0.03, -ww / 2, zz)), bm.verts.new((x - 0.03, ww / 2, zz)))) for i in range(n - 1): bm.faces.new((fr[i][0], fr[i][1], fr[i + 1][1], fr[i + 1][0])) bm.faces.new((bk[i][1], bk[i][0], bk[i + 1][0], bk[i + 1][1])) bm.faces.new((bk[i][0], fr[i][0], fr[i + 1][0], bk[i + 1][0])) bm.faces.new((fr[i][1], bk[i][1], bk[i + 1][1], fr[i + 1][1])) bm.faces.new((fr[-1][0], fr[-1][1], bk[-1][1], bk[-1][0])) bm.faces.new((bk[0][0], bk[0][1], fr[0][1], fr[0][0])) me = bpy.data.meshes.new(nm) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(nm, me) link(ob) return assign(ob, M_MARBLE) for ring, (z0, h, w, rr, off) in enumerate(((12.25, 0.75, 0.5, 0.64, 0.0), (12.6, 0.85, 0.48, 0.68, 0.5))): for j in range(8): a = 2 * math.pi * (j + off) / 8 lf = leaf(f'{name}_Leaf_{ring}_{j}', h, w) lf.location = (rr * math.cos(a), rr * math.sin(a), z0) lf.rotation_euler = (0, 0, a) parts.append(lf) for j in range(4): # 모서리 소용돌이(볼류트) a = math.pi / 4 + j * math.pi / 2 parts.append(sphere(f'{name}_Volute_{j}', r=0.17, loc=(0.92 * math.cos(a), 0.92 * math.sin(a), 13.62), material=M_MARBLE, seg=12, rings=8, scale=(1, 0.6, 1))) return join(parts, name)col_g = make_column('Column_Grey_Granite', M_GRAN_G)col_p = make_column('Column_Pink_Granite', M_GRAN_P)col_g.location = (XS[0], ROW_Y[0], STYLO)for i, x in enumerate(XS[1:], 1): duplicate(col_g, loc=(x, ROW_Y[0], STYLO), name=f'Column_Front_{i + 1}')col_g.name = 'Column_Front_1'first = Truefor r_i, y in enumerate(ROW_Y[1:], 2): for i in (0, 2, 5, 7): if first: col_p.location = (XS[i], y, STYLO) col_p.name = f'Column_Row{r_i}_{i + 1}' first = False else: duplicate(col_p, loc=(XS[i], y, STYLO), name=f'Column_Row{r_i}_{i + 1}')# 블록 앞면 벽기둥(안타)for s in (-1, 1): box(f'Anta_Pilaster_{"L" if s < 0 else "R"}', size=(1.7, 0.5, COL_H), loc=(s * (BX - 1.0), B_FRONT - 0.25, STYLO), material=M_MARBLE, base=True)# 엔타블러처EX = 16.55EF = ROW_Y[0] - 0.8box('Architrave', size=(2 * EX, B_FRONT - EF, 1.3), loc=(0, (EF + B_FRONT) / 2, ENT_Z), material=M_MARBLE, base=True)box('Frieze', size=(2 * EX - 0.2, B_FRONT - EF - 0.1, 1.25), loc=(0, (EF + B_FRONT) / 2 + 0.05, ENT_Z + 1.3), material=M_MARBLE, base=True)box('Cornice_Bed', size=(2 * EX + 0.5, B_FRONT - EF + 0.25, 0.4), loc=(0, (EF + B_FRONT) / 2 - 0.125, ENT_Z + 2.55), material=M_MARBLE, base=True)CX = EX + 0.6CF = EF - 0.6box('Cornice_Corona', size=(2 * CX, B_FRONT - CF, 0.65), loc=(0, (CF + B_FRONT) / 2, ENT_Z + 2.95), material=M_MARBLE, base=True)ENT_TOP = ENT_Z + 3.6 # 19.3# 모딜리온(코니스 받침) 배열mod = box('Modillion', size=(0.28, 0.5, 0.25), material=M_MARBLE, base=True)k = 0n_front = 46for i in range(n_front): x = -CX + 0.4 + (2 * CX - 0.8) * i / (n_front - 1) duplicate(mod, loc=(x, EF - 0.25 + 0.0, ENT_Z + 2.7), name=f'Modillion_Front_{i}')for s in (-1, 1): n_side = 20 for i in range(n_side): y = EF + 0.4 + (B_FRONT - EF - 0.8) * i / (n_side - 1) duplicate(mod, loc=(s * (EX + 0.25), y, ENT_Z + 2.7), rot=(0, 0, math.pi / 2), name=f'Modillion_Side_{s}_{i}')# 경사 코니스 아래 모딜리온a23 = math.radians(23.0)for sd in (-1, 1): for i in range(18): t = (i + 0.5) / 18 x = sd * (CX - 0.9) * (1 - t) zz = ENT_TOP + (0.9 + (CX - 0.9) * t) * TAN - 0.62 - 0.25 duplicate(mod, loc=(x, CF + 0.55, zz), rot=(0, sd * a23, 0), name=f'Modillion_Rake_{sd}_{i}')bpy.data.objects.remove(mod, do_unlink=True)# 명문: M·AGRIPPA·L·F·COS·TERTIVM·FECITtxt = text3d('Inscription_Agrippa', 'M·AGRIPPA·L·F·COS·TERTIVM·FECIT', size=0.95, depth=0.04, loc=(0, EF + 0.08, ENT_Z + 1.62), rot=(math.pi / 2, 0, 0), material=M_INSCR)txt.data.space_character = 1.15# 포르티코 박공 + 지붕PW = CXextrude_profile('Portico_Pediment_Tympanum', [(-PW + 0.3, 0), (PW - 0.3, 0), (0, (PW - 0.3) * TAN)], depth=B_FRONT - CF - 0.8, loc=(0, (CF + 0.8 + B_FRONT) / 2, ENT_TOP - 0.05), material=M_MARBLE)gable_roof('Portico', PW, ENT_TOP, CF, B_FRONT + 0.3)# 청동 문과 문틀, 양옆 벽감box('Door_Frame_Marble', size=(7.6, 0.5, 11.4), loc=(0, B_FRONT - 0.2, STYLO), material=M_MARBLE, base=True)box('Bronze_Door_Leaves', size=(5.6, 0.4, 9.0), loc=(0, B_FRONT - 0.5, STYLO), material=M_BRONZE, base=True)box('Door_Grille', size=(5.6, 0.3, 1.8), loc=(0, B_FRONT - 0.48, STYLO + 9.2), material=M_BRONZE, base=True)box('Door_Leaf_Seam', size=(0.08, 0.06, 9.0), loc=(0, B_FRONT - 0.73, STYLO), material=M_INSCR, base=True)for s in (-1, 1): x = s * 11.1 side = 'L' if s < 0 else 'R' box(f'Apse_Niche_{side}', size=(5.0, 0.1, 8.0), loc=(x, B_FRONT - 0.04, STYLO), material=M_NICHE, base=True) cylinder(f'Apse_Niche_Arch_{side}', r=2.5, h=0.1, loc=(x, B_FRONT - 0.04, STYLO + 8.0), rot=(math.pi / 2, 0, 0), material=M_NICHE, seg=32, base=False)# ── 로툰다 벽의 벽돌 하중분산 아치(블라인드 아치) ──────def arch_mesh(name, rad, tube, ang=math.pi): bm = bmesh.new() n, m = 20, 8 rings = [] for i in range(n + 1): t = (math.pi - ang) / 2 + ang * i / n cx, cz = rad * math.cos(t), rad * math.sin(t) ring = [] for j in range(m): b = 2 * math.pi * j / m rr = rad + tube * math.cos(b) ring.append(bm.verts.new((tube * math.sin(b), rr * math.cos(t), rr * math.sin(t)))) rings.append(ring) for i in range(n): for j in range(m): bm.faces.new((rings[i][j], rings[i + 1][j], rings[i + 1][(j + 1) % m], rings[i][(j + 1) % m])) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) link(ob) smooth(ob) return assign(ob, M_ARCH)for zone, (zc, rad, count, ang) in enumerate(((9.0, 3.6, 18, math.pi), (21.4, 2.2, 28, math.pi * 0.8), (27.4, 2.0, 32, math.pi * 0.8))): proto = arch_mesh(f'Relieving_Arch_Z{zone}', rad, 0.22, ang) used = False for j in range(count): az = 2 * math.pi * (j + 0.5) / count x, y = (R_OUT - 0.08) * math.cos(az), (R_OUT - 0.08) * math.sin(az) if y < 0 and abs(x) < BX + rad + 0.6 and zc < apex_b: # 중간 블록과 겹치는 곳은 건너뜀 continue if not used: proto.location, proto.rotation_euler, used = (x, y, zc), (0, 0, az), True else: duplicate(proto, loc=(x, y, zc), rot=(0, 0, az), name=f'Relieving_Arch_Z{zone}_{j}')# 중간 블록 옆면 벽감for s in (-1, 1): side = 'L' if s < 0 else 'R' for k, yc in enumerate((-30.0, -25.2)): box(f'Block_Side_Niche_{side}{k}', size=(0.1, 3.2, 6.0), loc=(s * (BX + 0.04), yc, 6.0), material=M_NICHE, base=True) cylinder(f'Block_Side_Niche_Arch_{side}{k}', r=1.6, h=0.1, loc=(s * (BX + 0.04), yc, 12.0), rot=(0, math.pi / 2, 0), material=M_NICHE, seg=24, base=False) box(f'Block_Side_Niche_Sill_{side}{k}', size=(0.3, 3.8, 0.3), loc=(s * (BX + 0.1), yc, 5.7), material=M_MARBLE, base=True)






