본문으로 건너뛰기
3D 아틀리에
성 바실리 대성당 3D 모형

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

이 모형에 대하여

성 바실리 대성당은 모스크바 붉은 광장 남쪽 끝에 서 있는 러시아 정교회 성당으로, 정식 이름은 '해자 위의 성모 보호 대성당'이다. 이반 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.5
C8 = 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 ob
def 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 out
ONION = 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 ob
def 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 ob
def 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 out
def 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 = None
for 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.0
D_SMALL = 9.6
big = [
('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.4
z0 = PLAT_TOP - 0.3
ctop = 22.0
octa('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 * S8
for 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.05
for 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.7
dz0 = tz - 0.4
dz1 = dz0 + 4.4
octa('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.05
tz1 = tz0 + 9.4
tent('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.8
box('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.6
for 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)

다른 모형