
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
성 바실리 대성당은 모스크바 붉은 광장 남쪽 끝에 서 있는 러시아 정교회 성당으로, 정식 이름은 '해자 위의 성모 보호 대성당'이다. 이반 4세가 카잔 한국 정복을 기념해 세우게 했으며 1555년부터 1561년 사이에 지어졌다. 뒤에 성자 바실리를 모신 예배당이 더해지면서 지금의 이름으로 불리게 되었다.
건물은 하나의 큰 내부 공간 대신 여러 개의 탑형 예배당을 한 기단 위에 모은 구성이다. 가운데에 팔각 천막 지붕(샤툐르)을 얹은 성모 보호 교회가 가장 높이 솟고, 동서남북 축에 큰 팔각 예배당 넷, 그 사이 대각선에 작은 예배당 넷이 놓인다. 붉은 벽돌 벽에 흰 돌로 띠와 창틀, 용골형 장식 박공(코코시니크)을 둘렀고, 예배당마다 무늬와 색이 다른 양파 돔을 얹었다. 지금의 다채로운 색채는 17세기 이후 보수와 덧칠을 거치며 갖춰졌고, 천막 지붕의 종탑도 17세기에 세워졌다.
이 모델은 높이 약 6.5m 의 기단과 회랑 난간, 그 위의 아홉 예배당, 남동쪽 종탑, 정면의 지붕 덮인 계단 현관 두 곳을 실제 크기에 가깝게 재현했다. 돔의 나선·줄무늬·비늘 무늬와 천막 지붕의 마름모 격자는 형태를 읽기 쉽게 하는 데 초점을 두었다.
만든 방법
양파 돔은 Catmull-Rom 으로 다듬은 단면을 bmesh 로 직접 회전시켜 만들었고, 꼭짓점 각도를 높이에 비례해 비틀어 나선을 만든 뒤 면마다 재질 번호를 배정해 줄무늬와 비늘 무늬를 냈다. 띠마다 반경을 살짝 부풀려 골이 진 입체감을 더했다. 코코시니크와 아치 창은 용골형 측면 프로필을 돌출한 부품 하나를 만들어 팔각 벽면마다 링크 복제로 배치했다. 기단은 십자형 평면 다각형을 돌출하고, 같은 윤곽을 안팎으로 오프셋한 고리 벽으로 띠와 난간을 만들었다. 실제 돔별 무늬와 색 배치, 예배당 내부, 회랑 지붕, 벽면의 세밀한 벽돌 장식은 단순화했으며 예배당 위치도 대칭으로 정리했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 500줄.
# 성 바실리 대성당(포크로프스키 대성당, 모스크바 붉은 광장) — 코드로만 만든 모델.# 높은 천막 지붕(샤툐르)의 중앙 교회를 큰 팔각 예배당 4개(동서남북)와 작은 예배당 4개(대각선)가 둘러싼# 9개 예배당 구성을 높이 6.5m 의 기단 위에 세웠다. 양파 돔은 bmesh 로 직접 짠 회전체이며,# 꼭짓점을 높이에 따라 비틀고 면마다 다른 재질을 배정해 나선·줄무늬·비늘 무늬를 만들었다(텍스처 없음).# 코코시니크(용골형 장식 박공)는 측면 프로필을 돌출한 판을 팔각 벽면마다 배열했고,# 같은 부품은 링크 복제로 메시를 공유한다. 종탑과 지붕 덮인 계단 현관 두 곳을 더했다. 단위 m, 높이 약 47.5m.from atelier import *SHOT = {"azimuth": -38, "elevation": 12, "lens": 55, "light": "golden", "background": "#1f2a36"}# ---------------------------------------------------------------- 재질BRICK = mat('Brick_Red', '#9a3b2b', 0.85)STONE = mat('Whitestone_Trim', '#e4d9c3', 0.7)GOLD = mat('Gilded_Metal', '#d8a943', 0.28, 1.0)GREEN = mat('Dome_Green', '#2c7a4b', 0.4)YELLOW = mat('Dome_Yellow', '#dcb43a', 0.42)RED = mat('Dome_Red', '#b5302b', 0.42)BLUE = mat('Dome_Blue', '#2e5c9c', 0.42)CREAM = mat('Dome_Cream', '#efe7d2', 0.42)ROOF = mat('Roof_Green_Metal', '#3f6b52', 0.5, 0.2)WINDOW = mat('Window_Dark', '#231c1b', 0.35)STEP = mat('Granite_Steps', '#8b877e', 0.8)TENT = mat('Tent_Ochre', '#c99a52', 0.6)PLAT_TOP = 6.5C8 = math.cos(math.pi / 8)S8 = math.sin(math.pi / 8)def set_mats(ob, mats): ob.data.materials.clear() for m in mats: ob.data.materials.append(m) return obdef bm_obj(name, bm, mats, loc=(0, 0, 0), smooth_on=False): me = bpy.data.meshes.new(name) for m in mats: me.materials.append(m) # 재질 슬롯을 먼저 만들어야 면별 재질 번호가 유지된다 bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) link(ob) ob.location = Vector(loc) if smooth_on: smooth(ob) return ob나머지 452줄 펼치기접기
def catmull(pts, sub=3): """(r,z) 제어점을 Catmull-Rom 으로 부드럽게.""" out = [] n = len(pts) for i in range(n - 1): p0 = pts[max(i - 1, 0)] p1, p2 = pts[i], pts[i + 1] p3 = pts[min(i + 2, n - 1)] for s in range(sub): t = s / sub t2, t3 = t * t, t * t * t out.append(tuple(0.5 * ((2 * p1[k]) + (-p0[k] + p2[k]) * t + (2 * p0[k] - 5 * p1[k] + 4 * p2[k] - p3[k]) * t2 + (-p0[k] + 3 * p1[k] - 3 * p2[k] + p3[k]) * t3) for k in range(2))) out.append(pts[-1]) return outONION = catmull([(0.80, 0.00), (0.90, 0.06), (0.98, 0.15), (1.00, 0.25), (0.97, 0.35), (0.86, 0.46), (0.68, 0.56), (0.48, 0.65), (0.31, 0.73), (0.18, 0.81), (0.10, 0.88), (0.055, 0.94), (0.0, 1.0)], 3)def onion(name, rmax, H, loc, mats, pattern='spiral', bands=12, twist=1.0, seg=48, rib=0.035): """양파 돔. pattern: spiral(비틀린 줄무늬) / stripe(세로 줄무늬) / scale(비늘 체크) / plain. 줄무늬 띠마다 반경을 살짝 부풀려 골(리브)이 진 입체감을 준다.""" bm = bmesh.new() rows = [] prof = ONION per = seg // bands if pattern in ('plain', 'scale'): rib = 0.0 for j, (rn, zn) in enumerate(prof[:-1]): ring = [] for i in range(seg): a = 2 * math.pi * i / seg + twist * math.pi * zn rr = rn * rmax * (1 + rib * math.sin(math.pi * (i % per) / per)) ring.append(bm.verts.new((rr * math.cos(a), rr * math.sin(a), zn * H))) rows.append(ring) apex = bm.verts.new((0, 0, H)) for j in range(len(rows) - 1): for i in range(seg): f = bm.faces.new((rows[j][i], rows[j][(i + 1) % seg], rows[j + 1][(i + 1) % seg], rows[j + 1][i])) if pattern in ('spiral', 'stripe'): f.material_index = (i // per) % len(mats) elif pattern == 'scale': f.material_index = ((i // per) + (j // 3)) % len(mats) else: f.material_index = 0 for i in range(seg): f = bm.faces.new((rows[-1][i], rows[-1][(i + 1) % seg], apex)) f.material_index = (i // per) % len(mats) if pattern != 'plain' else 0 # 아래 덮개 bot = bm.faces.new(list(reversed(rows[0]))) bot.material_index = 0 bmesh.ops.recalc_face_normals(bm, faces=bm.faces) ob = bm_obj(name, bm, mats, loc, smooth_on=True) bot_poly = ob.data.polygons[-1] bot_poly.use_smooth = False return obdef octa(name, R, z0, z1, loc, material, R1=None): """면이 축 방향을 향하는 팔각기둥(외접반경 R).""" ob = cylinder(name, R, z1 - z0, (loc[0], loc[1], z0), (0, 0, math.pi / 8), material, seg=8, r2=R1) return ob# ---------------------------------------------------------------- 공유 부품(링크 복제용)def keel_profile(w, h, n=10): """용골형(끝이 뾰족한) 아치 박공의 XZ 프로필.""" hw = w / 2 pts = [(-hw, 0.0), (hw, 0.0)] shoulder = h * 0.34 right = [] for k in range(n + 1): t = k / n a = math.pi * 0.0 + t * math.pi * 0.62 x = hw * math.cos(a) z = shoulder + hw * math.sin(a) * 0.9 right.append((x, z)) # 뾰족 끝으로 오목하게 x_end, z_end = right[-1] for k in range(1, 5): t = k / 4 x = x_end * (1 - t) z = z_end + (h - z_end) * (t ** 0.75) right.append((x, z)) left = [(-x, z) for x, z in reversed(right[:-1])] return pts + right + left_SHARED = {}def kokoshnik_mesh(w, h, depth): key = ('k', round(w, 3), round(h, 3), round(depth, 3)) if key not in _SHARED: ob = extrude_profile('Kokoshnik', keel_profile(w, h), depth, material=STONE) inner = extrude_profile('KokoshnikInset', keel_profile(w * 0.72, h * 0.78), depth * 0.5, loc=(0, depth * 0.5, h * 0.08), material=BRICK) j = join([ob, inner], 'Kokoshnik') _SHARED[key] = j j.hide_render = True j.hide_viewport = True j['atelier_cutter'] = True # 원본 템플릿은 숨김 return _SHARED[key]def place_on_face(template, name, cx, cy, ang, dist, z, scale=None): ob = duplicate(template, name=name) ob.hide_render = False ob.hide_viewport = False if 'atelier_cutter' in ob: del ob['atelier_cutter'] ob.location = (cx + dist * math.cos(ang), cy + dist * math.sin(ang), z) ob.rotation_euler = (0, 0, ang - math.pi / 2) if scale: ob.scale = scale return obdef window_mesh(w, h): key = ('w', round(w, 3), round(h, 3)) if key not in _SHARED: frame = box('WinFrame', (w * 1.5, 0.16, h * 1.15), (0, 0, 0), material=STONE, base=True) cap = extrude_profile('WinCap', keel_profile(w * 1.7, h * 0.55), 0.2, loc=(0, 0, h * 1.12), material=STONE) glass_ = box('WinGlass', (w, 0.1, h), (0, 0.06, h * 0.08), material=WINDOW, base=True) j = join([frame, cap, glass_], 'Window') j.hide_render = True j.hide_viewport = True j['atelier_cutter'] = True _SHARED[key] = j return _SHARED[key]def cross_mesh(): if 'cross' not in _SHARED: parts = [ box('CrossPost', (0.14, 0.14, 3.0), (0, 0, 0), material=GOLD, base=True), box('CrossBar', (1.3, 0.12, 0.13), (0, 0, 2.05), material=GOLD), box('CrossTop', (0.7, 0.12, 0.12), (0, 0, 2.55), material=GOLD), box('CrossFoot', (0.8, 0.12, 0.12), (0, 0, 1.15), rot=(0, math.radians(-22), 0), material=GOLD), sphere('CrossApple', 0.22, (0, 0, 0.15), material=GOLD, seg=16, rings=8), ] j = join(parts, 'Cross') j.hide_render = True j.hide_viewport = True j['atelier_cutter'] = True _SHARED['cross'] = j return _SHARED['cross']def put_cross(name, x, y, z, s=1.0): ob = duplicate(cross_mesh(), name=name) ob.hide_render = False ob.hide_viewport = False del ob['atelier_cutter'] ob.location = (x, y, z) ob.scale = (s, s, s) ob.rotation_euler = (0, 0, 0.3) return ob# ---------------------------------------------------------------- 기단(포드클레트)OUTLINE = [(13, -13), (13, -7), (20, -7), (20, 7), (13, 7), (13, 13), (7, 13), (7, 20), (-7, 20), (-7, 13), (-13, 13), (-13, 7), (-20, 7), (-20, -7), (-13, -7), (-13, -13), (-7, -13), (-7, -20), (7, -20), (7, -13)]def offset_poly(pts, d): """반시계 다각형을 안쪽으로 d 만큼(음수면 바깥) 오프셋 — 직각 다각형용.""" n = len(pts) out = [] for i in range(n): p0, p1, p2 = Vector((*pts[i - 1], 0)), Vector((*pts[i], 0)), Vector((*pts[(i + 1) % n], 0)) e1 = (p1 - p0).normalized() e2 = (p2 - p1).normalized() n1 = Vector((-e1.y, e1.x, 0)) n2 = Vector((-e2.y, e2.x, 0)) out.append(((p1 + d * (n1 + n2) / (1 + n1.dot(n2))).x, (p1 + d * (n1 + n2) / (1 + n1.dot(n2))).y)) return outdef ring_wall(name, outer, inner, z0, z1, material): bm = bmesh.new() n = len(outer) ob_ = [bm.verts.new((x, y, z0)) for x, y in outer] ot = [bm.verts.new((x, y, z1)) for x, y in outer] ib = [bm.verts.new((x, y, z0)) for x, y in inner] it = [bm.verts.new((x, y, z1)) for x, y in inner] for i in range(n): k = (i + 1) % n bm.faces.new((ob_[i], ob_[k], ot[k], ot[i])) bm.faces.new((ib[k], ib[i], it[i], it[k])) bm.faces.new((ot[i], ot[k], it[k], it[i])) bm.faces.new((ob_[k], ob_[i], ib[i], ib[k])) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) return bm_obj(name, bm, [material])platform = prism('Podklet_Base', OUTLINE, PLAT_TOP, material=BRICK)prism('Gallery_Paving', offset_poly(OUTLINE, 0.3), 0.12, loc=(0, 0, PLAT_TOP - 0.05), material=STEP)ring_wall('Podklet_Plinth', offset_poly(OUTLINE, -0.25), offset_poly(OUTLINE, 0.6), 0.0, 0.7, STONE)ring_wall('Podklet_Cornice', offset_poly(OUTLINE, -0.22), offset_poly(OUTLINE, 0.6), 5.7, 6.2, STONE)ring_wall('Gallery_Parapet', offset_poly(OUTLINE, 0.0), offset_poly(OUTLINE, 0.45), PLAT_TOP - 0.2, PLAT_TOP + 1.1, BRICK)ring_wall('Gallery_Parapet_Cap', offset_poly(OUTLINE, -0.08), offset_poly(OUTLINE, 0.53), PLAT_TOP + 1.1, PLAT_TOP + 1.28, STONE)# 기단 아래층 아치 창(어두운 아치 판)arch_t = Nonefor i in range(len(OUTLINE)): p0, p1 = Vector((*OUTLINE[i], 0)), Vector((*OUTLINE[(i + 1) % len(OUTLINE)], 0)) e = p1 - p0 L = e.length d = e.normalized() nrm = Vector((d.y, -d.x, 0)) # 바깥쪽 ang = math.atan2(nrm.y, nrm.x) cnt = int(L // 3.4) if cnt < 1: continue for k in range(cnt): t = (k + 0.5) / cnt p = p0 + e * t w = window_mesh(0.9, 1.6) place_on_face(w, 'Podklet_Window', p.x, p.y, ang, 0.06, 2.0)# ---------------------------------------------------------------- 예배당def drum_arcade(prefix, cx, cy, r, z0, z1, n=16): """돔 아래 원통 드럼 + 흰 띠 + 벽기둥(블라인드 아케이드).""" cylinder(prefix + '_Drum', r, z1 - z0, (cx, cy, z0), material=BRICK, seg=32) cylinder(prefix + '_DrumBase', r + 0.18, 0.35, (cx, cy, z0), material=STONE, seg=32) cylinder(prefix + '_DrumCornice', r + 0.2, 0.3, (cx, cy, z1 - 0.3), material=STONE, seg=32) pil = cylinder(prefix + '_Pilaster', 0.09 * r, z1 - z0 - 0.6, (0, 0, z0 + 0.35), material=STONE, seg=8) for k in range(n): a = 2 * math.pi * (k + 0.5) / n ob = pil if k == 0 else duplicate(pil, name=prefix + '_Pilaster') ob.location = (cx + r * math.cos(a), cy + r * math.sin(a), z0 + 0.35) for k in range(4): a = math.pi / 4 + k * math.pi / 2 + 2 * math.pi / n * 0 w = window_mesh(0.22 * r, 0.42 * (z1 - z0)) place_on_face(w, prefix + '_DrumWindow', cx, cy, a, r - 0.03, z0 + 0.22 * (z1 - z0))def chapel(prefix, cx, cy, R, top, dome_r, dome_h, mats, pattern, twist, bands, drum_r, drum_h, cross_s): z0 = PLAT_TOP - 0.3 octa(prefix + '_Octagon', R, z0, top, (cx, cy), BRICK) ap = R * C8 side = 2 * R * S8 # 흰 돌 띠(코니스) for zz, hh in ((z0 + 0.3, 0.45), (z0 + (top - z0) * 0.45, 0.35), (top - 0.45, 0.45)): octa(prefix + '_Cornice', R + 0.22, zz, zz + hh, (cx, cy), STONE) # 모서리 벽기둥 for k in range(8): a = math.pi / 8 + k * math.pi / 4 b = box(prefix + '_CornerPier', (0.35, 0.35, top - z0 - 0.4), (cx + (R + 0.02) * math.cos(a), cy + (R + 0.02) * math.sin(a), z0 + 0.3), (0, 0, a), STONE, base=True) # 창 wz = z0 + (top - z0) * 0.55 for k in range(8): a = k * math.pi / 4 w = window_mesh(side * 0.18, (top - z0) * 0.22) place_on_face(w, prefix + '_Window', cx, cy, a, ap + 0.02, wz) # 코코시니크 1단 kk = kokoshnik_mesh(side * 0.96, side * 0.85, 0.5) for k in range(8): place_on_face(kk, prefix + '_Kokoshnik', cx, cy, k * math.pi / 4, ap + 0.05, top - 0.05) # 안쪽 단 + 코코시니크 2단(엇갈림) R2 = drum_r * 1.18 octa(prefix + '_Tier', R2, top - 0.1, top + side * 0.75, (cx, cy), BRICK) s2 = 2 * R2 * S8 k2 = kokoshnik_mesh(s2 * 0.95, s2 * 0.8, 0.4) for k in range(8): a = k * math.pi / 4 place_on_face(k2, prefix + '_Kokoshnik2', cx, cy, a, R2 * C8 + 0.05, top + side * 0.75 - 0.15) dz0 = top + side * 0.75 - 0.15 drum_arcade(prefix, cx, cy, drum_r, dz0, dz0 + drum_h) dtop = dz0 + drum_h onion(prefix + '_OnionDome', dome_r, dome_h, (cx, cy, dtop - 0.05), mats, pattern, bands, twist) put_cross(prefix + '_Cross', cx, cy, dtop + dome_h - 0.25, cross_s)D_BIG = 14.0D_SMALL = 9.6big = [ ('Chapel_West_EntryIntoJerusalem', (-D_BIG, 0), [GREEN, YELLOW], 'spiral', 1.1, 12), ('Chapel_East_HolyTrinity', (D_BIG, 0), [RED, CREAM], 'spiral', -1.0, 12), ('Chapel_South_StNicholas', (0, -D_BIG), [GREEN, RED, YELLOW], 'scale', 0.0, 12), ('Chapel_North_StCyprian', (0, D_BIG), [BLUE, CREAM], 'spiral', 1.0, 12),]for name, (x, y), mats, pat, tw, bd in big: chapel(name, x, y, 4.6, 17.0, 3.75, 7.0, mats, pat, tw, bd, 2.55, 4.6, 1.05)small = [ ('Chapel_SW_AlexanderSvirsky', (-D_SMALL, -D_SMALL), [YELLOW, GREEN], 'stripe', 0.0, 16), ('Chapel_SE_VarlaamKhutynsky', (D_SMALL, -D_SMALL), [RED, YELLOW], 'spiral', -1.2, 12), ('Chapel_NE_ThreePatriarchs', (D_SMALL, D_SMALL), [GREEN, CREAM], 'scale', 0.0, 16), ('Chapel_NW_GregoryArmenia', (-D_SMALL, D_SMALL), [BLUE, YELLOW], 'spiral', 1.2, 12),]for name, (x, y), mats, pat, tw, bd in small: chapel(name, x, y, 2.9, 14.5, 2.5, 5.0, mats, pat, tw, bd, 1.65, 3.4, 0.8)# ---------------------------------------------------------------- 중앙 교회(포크로프) — 천막 지붕CR = 6.4z0 = PLAT_TOP - 0.3ctop = 22.0octa('Central_Octagon', CR, z0, ctop, (0, 0), BRICK)for zz, hh in ((14.0, 0.45), (ctop - 0.5, 0.5)): octa('Central_Cornice', CR + 0.25, zz, zz + hh, (0, 0), STONE)cside = 2 * CR * S8for k in range(8): a = math.pi / 8 + k * math.pi / 4 box('Central_CornerPier', (0.45, 0.45, ctop - z0 - 0.6), (CR * math.cos(a), CR * math.sin(a), z0 + 0.3), (0, 0, a), STONE, base=True) w = window_mesh(0.8, 2.6) place_on_face(w, 'Central_Window', 0, 0, k * math.pi / 4, CR * C8 + 0.02, 17.0)# 코코시니크 3단(점점 작아지며 엇갈림)tierR = [CR, CR * 0.86, CR * 0.74]tz = ctop - 0.05for t, Rt in enumerate(tierR): st = 2 * Rt * S8 kh = st * 0.8 if t > 0: octa('Central_Tier', Rt, tz - 0.6, tz + 0.05, (0, 0), BRICK) km = kokoshnik_mesh(st * 0.95, kh, 0.55) off = (math.pi / 8) * (t % 2) for k in range(8): place_on_face(km, 'Central_Kokoshnik', 0, 0, k * math.pi / 4 + off, Rt * C8 + 0.05 - (0.4 if t % 2 else 0), tz) tz += kh * 0.7# 팔각 드럼(창이 있는 팔각 고층부)dR = 4.7dz0 = tz - 0.4dz1 = dz0 + 4.4octa('Central_Drum', dR, dz0, dz1, (0, 0), BRICK)octa('Central_DrumCornice', dR + 0.22, dz1 - 0.35, dz1, (0, 0), STONE)octa('Central_DrumBase', dR + 0.18, dz0, dz0 + 0.35, (0, 0), STONE)for k in range(8): a = math.pi / 8 + k * math.pi / 4 box('Central_DrumPier', (0.35, 0.35, dz1 - dz0 - 0.6), (dR * math.cos(a), dR * math.sin(a), dz0 + 0.3), (0, 0, a), STONE, base=True) w = window_mesh(0.55, 1.9) place_on_face(w, 'Central_DrumWindow', 0, 0, k * math.pi / 4, dR * C8 + 0.02, dz0 + 0.9)# 천막 지붕: 팔각뿔, 면을 격자로 나눠 마름모 무늬def tent(name, R0, R1, z0, z1, loc, mats, cols=4, rows=14): bm = bmesh.new() grid = [] for j in range(rows + 1): t = j / rows R = R0 + (R1 - R0) * t ring = [] for k in range(8): a0 = math.pi / 8 + k * math.pi / 4 a1 = a0 + math.pi / 4 p0 = Vector((R * math.cos(a0), R * math.sin(a0), 0)) p1 = Vector((R * math.cos(a1), R * math.sin(a1), 0)) for c in range(cols): p = p0.lerp(p1, c / cols) ring.append(bm.verts.new((p.x, p.y, z0 + (z1 - z0) * t))) grid.append(ring) n = 8 * cols for j in range(rows): for i in range(n): f = bm.faces.new((grid[j][i], grid[j][(i + 1) % n], grid[j + 1][(i + 1) % n], grid[j + 1][i])) if len(mats) == 3: # 마름모 격자: 줄눈선 + 마름모 속 if (i + j) % 4 == 0 or (i - j) % 4 == 0: f.material_index = 1 elif (i + j) % 4 == 2 and (i - j) % 4 == 2: f.material_index = 2 else: f.material_index = 0 else: f.material_index = (i + j) % 2 if len(mats) > 1 else 0 bm.faces.new(list(reversed(grid[0]))) bm.faces.new(grid[-1]) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) return bm_obj(name, bm, mats, loc)tz0 = dz1 - 0.05tz1 = tz0 + 9.4tent('Central_TentRoof', dR + 0.05, 1.05, tz0, tz1, (0, 0, 0), [STONE, TENT, GREEN], 8, 28)# 천막 모서리 갈빗대for k in range(8): a = math.pi / 8 + k * math.pi / 4 pA = Vector(((dR + 0.12) * math.cos(a), (dR + 0.12) * math.sin(a), tz0)) pB = Vector((1.12 * math.cos(a), 1.12 * math.sin(a), tz1)) curve_tube('Central_TentRib', [tuple(pA), tuple(pB)], 0.1, material=GOLD)octa('Central_LanternBase', 1.25, tz1 - 0.1, tz1 + 0.3, (0, 0), STONE)cylinder('Central_Lantern', 0.95, 1.4, (0, 0, tz1 + 0.3), material=GOLD, seg=24)onion('Central_GoldenDome', 1.35, 2.5, (0, 0, tz1 + 1.65), [GOLD], 'plain')put_cross('Central_Cross', 0, 0, tz1 + 1.65 + 2.5 - 0.15, 0.75)# ---------------------------------------------------------------- 종탑(남동쪽, 17세기)bx, by = 15.8, 15.8box('BellTower_Base', (6.4, 6.4, 11.0), (bx, by, 0), material=BRICK, base=True)box('BellTower_BaseCornice', (6.8, 6.8, 0.45), (bx, by, 10.6), material=STONE, base=True)box('BellTower_Plinth', (6.7, 6.7, 0.7), (bx, by, 0), material=STONE, base=True)for sx in (-1, 1): for sy in (-1, 1): box('BellTower_Pier', (0.4, 0.4, 10.2), (bx + sx * 3.2, by + sy * 3.2, 0.5), material=STONE, base=True)octa('BellTower_Floor', 3.6, 11.0, 11.6, (bx, by), STONE)# 열린 종루: 팔각 기둥 8개bz0, bz1 = 11.6, 16.6for k in range(8): a = math.pi / 8 + k * math.pi / 4 box('BellTower_Column', (0.55, 0.55, bz1 - bz0), (bx + 3.0 * math.cos(a), by + 3.0 * math.sin(a), bz0), (0, 0, a), BRICK, base=True)cylinder('BellTower_Bell', 0.9, 1.4, (bx, by, bz1 - 2.3), material=GOLD, seg=24, r2=0.45)octa('BellTower_Entablature', 3.55, bz1, bz1 + 1.2, (bx, by), BRICK)octa('BellTower_EntCornice', 3.75, bz1 + 1.0, bz1 + 1.35, (bx, by), STONE)tent('BellTower_Tent', 3.4, 0.55, bz1 + 1.3, bz1 + 9.0, (bx, by, 0), [ROOF], 2, 10)for k in range(8): a = math.pi / 8 + k * math.pi / 4 curve_tube('BellTower_TentRib', [(bx + 3.48 * math.cos(a), by + 3.48 * math.sin(a), bz1 + 1.3), (bx + 0.6 * math.cos(a), by + 0.6 * math.sin(a), bz1 + 9.0)], 0.07, material=STONE) w = window_mesh(0.28, 0.75) place_on_face(w, 'BellTower_Dormer', bx, by, k * math.pi / 4, 2.55, bz1 + 3.6)for k in range(4): a = k * math.pi / 2 for zz in (2.0, 6.6): w = window_mesh(0.8, 1.9) place_on_face(w, 'BellTower_BaseWindow', bx, by, a, 3.22, zz)cylinder('BellTower_Lantern', 0.55, 0.9, (bx, by, bz1 + 8.9), material=STONE, seg=16)onion('BellTower_Dome', 0.8, 1.6, (bx, by, bz1 + 9.75), [GOLD], 'plain', seg=32)put_cross('BellTower_Cross', bx, by, bz1 + 9.75 + 1.5, 0.55)# ---------------------------------------------------------------- 지붕 덮인 계단 현관 2곳(남쪽 정면)for sgn in (-1, 1): xc = sgn * 8.6 pre = 'Porch_' + ('W' if sgn < 0 else 'E') # 계단(톱니 형태) steps = [] nst = 14 for s in range(nst): h = PLAT_TOP * (s + 1) / nst y = -20.6 + s * (7.2 / nst) steps.append(box(pre + '_Step', (2.6, 7.2 / nst + 0.01, h), (xc, y + 3.6 / nst, 0), material=STEP, base=True)) steps.append(box(pre + '_Landing', (2.6, 1.0, PLAT_TOP), (xc, -13.3, 0), material=STEP, base=True)) join(steps, pre + '_Stair') # 바깥 난간벽 extrude_profile(pre + '_StairWall', [(-20.6, 0), (-12.85, 0), (-12.85, PLAT_TOP + 1.2), (-20.6, 1.4)], 0.4, loc=(0, 0, 0), material=BRICK, axis='X').location = (xc + sgn * 1.5, 0, 0) # 계단 위 경사 지붕(기둥 + 판) for yy in (-20.2, -17.6, -15.0): zt = 1.0 + (yy + 20.6) / 7.2 * PLAT_TOP box(pre + '_Post', (0.3, 0.3, 3.4), (xc - sgn * 1.15, yy, zt - 0.6), material=BRICK, base=True) a = math.atan2(PLAT_TOP, 7.2) box(pre + '_Roof', (3.4, 7.9, 0.25), (xc, -17.0, 0.6 + 3.6 / 7.2 * PLAT_TOP + 3.1), rot=(a, 0, 0), material=ROOF) # 아래쪽 현관 탑(천막 지붕) box(pre + '_PorchBase', (3.6, 3.6, 3.6), (xc, -22.6, 0), material=BRICK, base=True) box(pre + '_PorchCornice', (3.9, 3.9, 0.3), (xc, -22.6, 3.4), material=STONE, base=True) w = window_mesh(0.9, 1.8) place_on_face(w, pre + '_Arch', xc, -22.6, -math.pi / 2, 1.82, 0.4) tent(pre + '_Tent', 2.0, 0.25, 3.7, 8.6, (xc, -22.6, 0), [ROOF], 2, 8) onion(pre + '_Dome', 0.45, 0.9, (xc, -22.6, 8.5), [GOLD], 'plain', seg=24) put_cross(pre + '_Cross', xc, -22.6, 9.3, 0.35)






