본문으로 건너뛰기
3D 아틀리에
숭례문 3D 모형

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

이 모형에 대하여

숭례문은 조선의 수도 한양을 둘러싼 도성의 남쪽 정문으로, 흔히 남대문이라 부른다. 1398년(태조 7년)에 처음 세워졌고 이후 여러 차례 고쳐 지었다. 1962년 국보로 지정되었으며, 지정번호가 쓰이던 때에는 국보 제1호였다.

2008년 2월 방화로 문루 2층 대부분과 1층 일부가 불탔다. 이후 전통 기법과 재료를 써서 복구 공사를 진행해 2013년 5월 다시 공개되었다.

건물은 무지개 모양 홍예문을 낸 화강암 석축과 그 위의 목조 문루로 이루어진다. 문루는 정면 5칸·측면 2칸의 2층 건물로, 네 면 모두 경사진 우진각 지붕을 얹었고 기둥 위에는 공포를 촘촘히 짜 올린 다포 양식을 썼다. 이 모델은 석축의 쌓기 켜와 홍예석, 석축 위 여장, 단청을 칠한 공포와 서까래, 백회를 바른 지붕마루, 상층 중앙의 세로 현판을 표현했다.

만든 방법

석축은 켜마다 조금씩 들여쌓은 화강암 블록을 고정 시드 난수로 길이를 나눠 만들고, EXACT 불리언으로 홍예 통로를 뚫은 뒤 부채꼴 홍예석과 측벽석을 따로 쌓았다. 지붕은 정사각 둘레를 직사각 고리로 옮기는 매개화로 우진각 면을 만들고 귀 쪽으로 갈수록 처마가 들리고 바깥으로 빠지도록 곡선을 주었으며, 수키와 줄·추녀마루·서까래는 지붕면 위를 따라 단면을 쓸어(sweep) 만들었다. 다포 공포 한 세트를 메시 공유 복제로 둘레에 배열했고 기둥도 같은 방식으로 복제했다. 현판 글씨는 실제 글자 대신 금색 획 조각으로 암시했고, 내부 구조·계단·주변 성곽은 생략했다. 치수는 실측 도면이 아니라 사진 비례에 맞춘 근사값이다.

build.py

이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 657줄.

# 숭례문(서울 남대문) — 석축 홍예문 위에 올린 정면 5칸·측면 2칸, 2층 우진각 지붕 문루.
# 석축은 켜마다 조금씩 들여 쌓은 화강암 블록을 난수(고정 시드)로 나눠 줄눈을 만들고, 불리언으로 홍예 통로를 뚫은 뒤
# 홍예석·측벽석을 따로 쌓았다. 지붕은 '정사각 둘레 → 직사각 고리' 매개화로 우진각 면을 만들고
# 처마 곡선(앙곡·안허리)을 귀 쪽으로 갈수록 들리게 했으며, 그 위에 수키와 줄을 한 줄씩 쓸어(sweep) 얹었다.
# 공포(다포)는 한 세트 메시를 연결 복제로 둘레에 배열했고, 기둥·창방·평방·단청 띠·난간·현판은 상자 조합으로 만들었다.
# 실측 도면이 아니라 사진 비례에 맞춘 근사 모델이다(단위 m).
from atelier import *
import random
random.seed(1398)
SHOT = {"azimuth": -56, "elevation": 8, "lens": 50, "light": "day", "background": "#4b5d72"}
# ---------------------------------------------------------------- 재질
GRANITE = mat('화강암', '#a19b8f', 0.85)
GRANITE2 = mat('화강암_짙은돌', '#8b867c', 0.9)
BRICK = mat('여장_회색전돌', '#7d7a74', 0.9)
TILE = mat('기와_회흑색', '#3b3e44', 0.72)
MORTAR = mat('양성바름_백회', '#e2ddd1', 0.85)
COL = mat('석간주_기둥', '#7a2e24', 0.6)
GREEN = mat('단청_뇌록', '#3f8a72', 0.6)
BLUE = mat('단청_군청', '#2d5b8e', 0.55)
RED = mat('단청_주홍', '#b5402d', 0.55)
OCHRE = mat('단청_황토', '#d5aa52', 0.55)
WOOD = mat('마루_목재', '#76583c', 0.7)
PLAQUE = mat('현판_바탕', '#22252d', 0.5)
GOLD = mat('현판_금박', '#c9a347', 0.35, 0.6)
class MB:
"""여러 상자·스윕을 한 메시로 모으는 작은 빌더(재질 슬롯 자동)."""
def __init__(self):
self.v, self.f, self.m, self.slots = [], [], [], []
def _mi(self, m):
if m not in self.slots:
self.slots.append(m)
return self.slots.index(m)
def face(self, idx, m):
self.f.append(list(idx))
self.m.append(self._mi(m))
def obox(self, c, ax, ay, az, m):
b = len(self.v)
for k in range(8):
sx = 1 if k & 1 else -1
나머지 609줄 펼치기
sy = 1 if k & 2 else -1
sz = 1 if k & 4 else -1
self.v.append(c + ax * sx + ay * sy + az * sz)
for q in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
self.face([b + i for i in q], m)
def box(self, mn, mx, m):
c = Vector(((mn[0] + mx[0]) / 2, (mn[1] + mx[1]) / 2, (mn[2] + mx[2]) / 2))
self.obox(c, Vector(((mx[0] - mn[0]) / 2, 0, 0)), Vector((0, (mx[1] - mn[1]) / 2, 0)),
Vector((0, 0, (mx[2] - mn[2]) / 2)), m)
def prism_y(self, pts, y0, y1, m):
"""XZ 다각형을 y0~y1 로 돌출."""
b = len(self.v)
n = len(pts)
for x, z in pts:
self.v.append(Vector((x, y0, z)))
for x, z in pts:
self.v.append(Vector((x, y1, z)))
self.face([b + i for i in range(n)], m)
self.face([b + n + i for i in reversed(range(n))], m)
for i in range(n):
j = (i + 1) % n
self.face([b + i, b + j, b + n + j, b + n + i], m)
def sweep(self, pts, prof, m, mcap0=None, mcap1=None):
"""점 목록을 따라 닫힌 단면 prof=[(가로, 법선)] 을 쓸어 만든다."""
rings = []
n = len(pts)
for i, p in enumerate(pts):
t = (pts[min(i + 1, n - 1)] - pts[max(i - 1, 0)]).normalized()
lat = t.cross(Vector((0, 0, 1)))
if lat.length < 1e-6:
lat = Vector((1, 0, 0))
lat.normalize()
nor = lat.cross(t).normalized()
if nor.z < 0:
nor = -nor
rings.append(len(self.v))
for a, b in prof:
self.v.append(p + lat * a + nor * b)
k = len(prof)
for i in range(n - 1):
for j in range(k):
j2 = (j + 1) % k
self.face([rings[i] + j, rings[i] + j2, rings[i + 1] + j2, rings[i + 1] + j], m)
self.face([rings[0] + j for j in reversed(range(k))], mcap0 or m)
self.face([rings[-1] + j for j in range(k)], mcap1 or m)
def build(self, name, recalc=True):
me = bpy.data.meshes.new(name)
me.from_pydata([tuple(v) for v in self.v], [], self.f)
for s in self.slots:
me.materials.append(s)
me.polygons.foreach_set('material_index', self.m)
me.update()
if recalc:
bm = bmesh.new()
bm.from_mesh(me)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
link(ob)
return ob
def ring_beams(mb, Wx, Wy, off, hw, z0, z1, m):
"""기둥선(Wx,Wy)에서 off 만큼 바깥에 도는 사각 띠(앞뒤 통짜, 옆면은 맞닿게)."""
ox, oy = Wx + off, Wy + off
for s in (-1, 1):
mb.box((-(ox + hw), s * oy - hw, z0), (ox + hw, s * oy + hw, z1), m)
mb.box((s * ox - hw, -(oy - hw), z0), (s * ox + hw, oy - hw, z1), m)
# ---------------------------------------------------------------- 석축(石築)과 홍예문
W, D, H = 30.0, 15.0, 8.0 # 석축 밑변 너비·깊이·높이
NC, BATTER = 11, 0.04 # 켜 수, 켜마다 들여쌓기
Z_PL, Z_MI = 0.35, H - 0.32 # 지대석 윗면, 미석 아랫면
CH = (Z_MI - Z_PL) / NC
R_IN, R_OUT, ZS = 2.75, 3.45, 3.95 # 홍예 안 반지름, 홍예석 바깥 반지름, 홍예 기점 높이
G = 0.022 # 줄눈
def half_at(k):
return W / 2 - k * BATTER, D / 2 - k * BATTER
def build_platform():
blocks = MB()
blocks.box((-W / 2 - 0.15, -D / 2 - 0.15, 0), (W / 2 + 0.15, D / 2 + 0.15, Z_PL), GRANITE2)
for k in range(NC):
hx, hy = half_at(k)
z0, z1 = Z_PL + k * CH, Z_PL + (k + 1) * CH
for s in (-1, 1):
# 앞·뒷면 블록
x = -hx
first = True
while x < hx - 0.05:
ln = random.uniform(0.9, 2.1) * (0.6 if first and k % 2 else 1.0)
first = False
x1 = min(hx, x + ln)
if hx - x1 < 0.6:
x1 = hx
pr = random.uniform(0, 0.025)
m = GRANITE if random.random() < 0.7 else GRANITE2
if s < 0:
blocks.box((x + G, -hy - pr, z0 + G), (x1 - G, -hy + 1.0, z1 - G), m)
else:
blocks.box((x + G, hy - 1.0, z0 + G), (x1 - G, hy + pr, z1 - G), m)
x = x1
# 옆면 블록
y = -hy + 1.0
while y < hy - 1.05:
y1 = min(hy - 1.0, y + random.uniform(0.9, 2.0))
if hy - 1.0 - y1 < 0.5:
y1 = hy - 1.0
pr = random.uniform(0, 0.025)
m = GRANITE if random.random() < 0.7 else GRANITE2
if s < 0:
blocks.box((-hx - pr, y + G, z0 + G), (-hx + 1.0, y1 - G, z1 - G), m)
else:
blocks.box((hx - 1.0, y + G, z0 + G), (hx + pr, y1 - G, z1 - G), m)
y = y1
ob_blocks = blocks.build('Seokchuk_Masonry')
core = MB()
core.box((-(W / 2 - 0.7), -(D / 2 - 0.7), 0.3), (W / 2 - 0.7, D / 2 - 0.7, H - 0.3), GRANITE2)
ob_core = core.build('Seokchuk_Core')
cut = MB()
prof = [(-R_OUT, -1.0), (R_OUT, -1.0)]
for i in range(25):
a = math.pi * i / 24
prof.append((R_OUT * math.cos(a), ZS + R_OUT * math.sin(a)))
cut.prism_y(prof, -D, D, GRANITE)
ob_cut = cut.build('Hongye_Cutter')
for o in (ob_blocks, ob_core):
boolean(o, ob_cut)
apply_all(o)
bpy.data.objects.remove(ob_cut, do_unlink=True)
# 미석(眉石)과 윗면 박석
hx, hy = half_at(NC)
top = MB()
top.box((-hx - 0.14, -hy - 0.14, Z_MI), (hx + 0.14, hy + 0.14, H), GRANITE)
# 통로 바닥 박석
top.box((-R_OUT - 0.05, -D / 2 - 0.15, 0), (R_OUT + 0.05, D / 2 + 0.15, 0.06), GRANITE2)
# 홍예 측벽석(기점 아래)
nj = 5
for side in (-1, 1):
for k in range(nj):
z0, z1 = k * ZS / nj, (k + 1) * ZS / nj
kk = (z0 + z1) / 2 / CH
hyk = D / 2 - kk * BATTER
y = -hyk - 0.02
while y < hyk:
y1 = min(hyk + 0.02, y + random.uniform(1.4, 2.2))
if hyk - y1 < 0.6:
y1 = hyk + 0.02
x_in, x_out = R_IN, R_OUT + 0.05
xa, xb = (x_in, x_out) if side > 0 else (-x_out, -x_in)
top.box((xa, y + G, max(z0, 0.06) + G), (xb, y1 - G, z1 - G),
GRANITE if random.random() < 0.6 else GRANITE2)
y = y1
# 홍예석(무지개돌) — 부채꼴 돌을 통로 깊이로 나눠 쌓음
NV = 13
for i in range(NV):
a0 = math.pi * i / NV + 0.006
a1 = math.pi * (i + 1) / NV - 0.006
pts = []
for j in range(3):
a = a0 + (a1 - a0) * j / 2
pts.append((R_IN * math.cos(a), ZS + R_IN * math.sin(a)))
for j in range(3):
a = a1 - (a1 - a0) * j / 2
pts.append(((R_OUT + 0.05) * math.cos(a), ZS + (R_OUT + 0.05) * math.sin(a)))
zm = ZS + (R_IN + R_OUT) / 2 * math.sin((a0 + a1) / 2)
hyk = D / 2 - (zm - Z_PL) / CH * BATTER + 0.03
segs = 7
for s in range(segs):
y0 = -hyk + 2 * hyk * s / segs + (0 if s == 0 else G)
y1 = -hyk + 2 * hyk * (s + 1) / segs - (0 if s == segs - 1 else G)
top.prism_y(pts, y0, y1, GRANITE if (i + s) % 3 else GRANITE2)
ob_top = top.build('Seokchuk_Mison_Hongyeseok')
# 성문 문짝(열린 상태로 측벽에 붙임)
door = MB()
for side in (-1, 1):
xa = side * (R_IN - 0.02)
xb = side * (R_IN - 0.16)
door.box((min(xa, xb), 1.2, 0.08), (max(xa, xb), 3.9, ZS + 0.55), COL)
for zz in (0.6, 1.6, 2.6, 3.4): # 띠쇠
door.box((min(xa, xb) - 0.015, 1.2 + 0.05, zz), (max(xa, xb) + 0.015, 3.9 - 0.05, zz + 0.12), GRANITE2)
door.build('Gate_Doors')
# 여장(女墻) — 석축 위 낮은 담과 기와 지붕돌
par = MB()
t, ph = 0.55, 1.05
ex, ey = hx - 0.05, hy - 0.05
segs = []
n = 9
for i in range(n):
x0 = -ex + 2 * ex * i / n
x1 = -ex + 2 * ex * (i + 1) / n
for s in (-1, 1):
segs.append(((x0 + (0.14 if i else 0), s * ey - t / 2), (x1 - (0.14 if i < n - 1 else 0), s * ey + t / 2)))
n2 = 4
for i in range(n2):
y0 = -(ey - t / 2) + 2 * (ey - t / 2) * i / n2
y1 = -(ey - t / 2) + 2 * (ey - t / 2) * (i + 1) / n2
for s in (-1, 1):
segs.append(((s * ex - t / 2, y0 + (0.14 if i else 0)), (s * ex + t / 2, y1 - (0.14 if i < n2 - 1 else 0))))
for (xa, ya), (xb, yb) in segs:
if xb - xa > yb - ya:
xa, xb = xa - (t / 2 if xa < -ex + 0.01 else 0), xb + (t / 2 if xb > ex - 0.01 else 0)
par.box((xa, ya, H), (xb, yb, H + ph), BRICK)
par.box((xa - 0.08, ya - 0.08, H + ph), (xb + 0.08, yb + 0.08, H + ph + 0.12), TILE)
# 총안(銃眼) 자리 — 작은 어두운 구멍판
if xb - xa > yb - ya:
for k in range(2):
cx = xa + (xb - xa) * (k + 1) / 3
par.box((cx - 0.12, ya - 0.02, H + 0.45), (cx + 0.12, yb + 0.02, H + 0.68), TILE)
par.build('Yeojang_Parapet')
return H
# ---------------------------------------------------------------- 지붕
def f_prof(t):
return 0.5 * t + 0.5 * t * t
class Roof:
def __init__(self, ax, ay, r, h, ze, lift, bulge, th=0.36):
self.ax, self.ay, self.r, self.h, self.ze = ax, ay, r, h, ze
self.lift, self.bulge, self.th = lift, bulge, th
def X(self, t):
return self.ax - (self.ax - self.r) * t
def Y(self, t):
return self.ay * (1 - t)
def pt(self, u, v, t, dz=0.0):
c = min(abs(u), abs(v)) ** 3
w = max(0.0, 1 - t / 0.55) ** 2
b = self.bulge * c * w
return Vector((u * (self.X(t) + b), v * (self.Y(t) + b),
self.ze + self.h * f_prof(t) + self.lift * c * w + dz))
def build_roof(name, R, tmax, t_purlin, full, n_figs):
Nf, Ne, K = 56, 28, 24
per = []
for i in range(Nf):
per.append((-1 + 2 * i / Nf, -1))
for i in range(Ne):
per.append((1, -1 + 2 * i / Ne))
for i in range(Nf):
per.append((1 - 2 * i / Nf, 1))
for i in range(Ne):
per.append((-1, 1 - 2 * i / Ne))
P = len(per)
ts = [tmax * (k / K) ** 1.2 for k in range(K + 1)]
sk = MB()
bases = []
for dz in (0.0, -R.th):
bases.append(len(sk.v))
for t in ts:
for u, v in per:
sk.v.append(R.pt(u, v, t, dz))
tb, bb = bases
for k in range(K):
for i in range(P):
a, b = k * P + i, k * P + (i + 1) % P
sk.face([tb + a, tb + b, tb + b + P, tb + a + P], TILE)
sk.face([bb + a + P, bb + b + P, bb + b, bb + a], COL)
for i in range(P):
j = (i + 1) % P
sk.face([bb + i, bb + j, tb + j, tb + i], TILE)
if not full:
o = K * P
sk.face([tb + o + i, tb + o + j, bb + o + j, bb + o + i], TILE)
sk.build(name + '_Surface', recalc=False)
# 수키와 줄
tl = MB()
pitch = 0.34
hp = [(0.13 * math.cos(math.pi * i / 6), 0.11 * math.sin(math.pi * i / 6) - 0.02) for i in range(7)]
span = R.ax - R.r
for v in (-1, 1):
x = -R.ax + pitch / 2
while x < R.ax:
lim = abs(x) / 0.975
if lim < R.ax:
te = min(tmax, (R.ax - lim) / span)
if te > 0.015:
pts = []
for j in range(19):
t = te * j / 18
u = max(-0.975, min(0.975, x / R.X(t)))
pts.append(R.pt(u, v, t, 0.0))
tl.sweep(pts, hp, TILE)
x += pitch
for u in (-1, 1):
y = -R.ay + pitch / 2
while y < R.ay:
te = min(tmax, 1 - abs(y) / (0.975 * R.ay))
if te > 0.015:
pts = []
for j in range(13):
t = te * j / 12
v = max(-0.975, min(0.975, y / R.Y(t)))
pts.append(R.pt(u, v, t, 0.0))
tl.sweep(pts, hp, TILE)
y += pitch
# 마루: 추녀마루(백회 양성 + 기와 덮개)
hip_body = [(-0.27, -0.2), (0.27, -0.2), (0.27, 0.42), (-0.27, 0.42)]
hip_cap = [(-0.32, 0.4), (0.32, 0.4), (0.32, 0.58), (-0.32, 0.58)]
tend = 0.985 if full else tmax
for su in (-1, 1):
for sv in (-1, 1):
pts = [R.pt(su, sv, -0.03 + (tend + 0.03) * j / 30) for j in range(31)]
tl.sweep(pts, hip_body, MORTAR)
tl.sweep(pts, hip_cap, TILE)
# 잡상
for q in range(n_figs):
t = 0.1 + q * 0.03
p = R.pt(su, sv, t) + Vector((0, 0, 0.62))
s = 0.1 if q else 0.13
tl.box((p.x - s, p.y - s, p.z - 0.05), (p.x + s, p.y + s, p.z + 0.22), TILE)
tl.box((p.x - s * 0.7, p.y - s * 0.7, p.z + 0.22), (p.x + s * 0.7, p.y + s * 0.7, p.z + 0.36), TILE)
# 추녀·사래(귀 서까래)
pts = [R.pt(su, sv, -0.07 + (t_purlin + 0.07) * j / 14, -R.th - 0.2) for j in range(15)]
tl.sweep(pts, [(-0.2, -0.22), (0.2, -0.22), (0.2, 0.2), (-0.2, 0.2)], GREEN, mcap0=OCHRE)
if full:
zt = R.ze + R.h * f_prof(0.985)
rr = R.r + 0.25
tl.box((-rr, -0.36, zt - 0.5), (rr, 0.36, zt + 0.55), MORTAR)
tl.box((-rr - 0.05, -0.43, zt + 0.55), (rr + 0.05, 0.43, zt + 0.78), TILE)
for s in (-1, 1): # 취두
x0 = s * (rr - 0.1)
prof = [(0, -0.2), (0.85, -0.2), (0.95, 0.5), (1.15, 0.95), (1.0, 1.35), (0.55, 1.55), (0.15, 1.35), (0, 1.0)]
pts = [(x0 + s * a, zt + b) for a, b in prof]
if s < 0:
pts.reverse()
tl.prism_y(pts, -0.42, 0.42, TILE)
tl.box((min(x0 + s * 0.2, x0 + s * 0.75), -0.47, zt + 0.25), (max(x0 + s * 0.2, x0 + s * 0.75), 0.47, zt + 0.4), MORTAR)
tl.build(name + '_Tiles_Ridges')
# 서까래(부연) — 처마 밑
rf = MB()
sq = [(-0.09, -0.09), (0.09, -0.09), (0.09, 0.09), (-0.09, 0.09)]
rp = 0.42
dz = -R.th - 0.085
tr = t_purlin + 0.05
for v in (-1, 1):
x = -R.ax + rp / 2
while x < R.ax:
lim = abs(x) / 0.95
if lim < R.ax:
te = min(tr, (R.ax - lim) / span)
if te > 0.01:
pts = []
for j in range(7):
t = -0.025 + (te + 0.025) * j / 6
u = max(-0.95, min(0.95, x / R.X(max(t, 0))))
pts.append(R.pt(u, v, t, dz))
rf.sweep(pts, sq, GREEN, mcap0=BLUE)
x += rp
for u in (-1, 1):
y = -R.ay + rp / 2
while y < R.ay:
te = min(tr, 1 - abs(y) / (0.95 * R.ay))
if te > 0.01:
pts = []
for j in range(7):
t = -0.025 + (te + 0.025) * j / 6
v = max(-0.95, min(0.95, y / R.Y(max(t, 0))))
pts.append(R.pt(u, v, t, dz))
rf.sweep(pts, sq, GREEN, mcap0=BLUE)
y += rp
rf.build(name + '_Rafters')
# ---------------------------------------------------------------- 목조부
def column_mesh():
return cylinder('Column_proto', r=0.32, h=1.0, seg=16, material=COL)
def make_bracket():
"""다포 공포 한 세트. 원점=벽선·공포대 바닥, 바깥 = -Y."""
b = MB()
b.box((-0.27, -0.27, 0.0), (0.27, 0.27, 0.27), GREEN) # 주두
b.box((-0.45, -0.13, 0.27), (0.45, 0.13, 0.45), BLUE) # 첨차 1
b.box((-0.13, -0.8, 0.27), (0.13, 0.35, 0.45), GREEN) # 살미 1
b.box((-0.13, -0.8, 0.27), (0.13, -0.62, 0.45), RED)
for x, y in ((-0.38, 0), (0.38, 0), (0, -0.62), (0, 0)):
b.box((x - 0.1, y - 0.1, 0.45), (x + 0.1, y + 0.1, 0.52), OCHRE) # 소로
b.box((-0.58, -0.13, 0.52), (0.58, 0.13, 0.7), BLUE) # 첨차 2
b.box((-0.58, -0.75, 0.52), (0.58, -0.49, 0.7), BLUE)
b.box((-0.13, -1.15, 0.52), (0.13, 0.35, 0.7), GREEN) # 살미 2
b.box((-0.135, -1.15, 0.52), (0.135, -0.97, 0.7), RED)
for x, y in ((-0.5, 0), (0.5, 0), (-0.5, -0.62), (0.5, -0.62), (0, -1.0)):
b.box((x - 0.1, y - 0.1, 0.7), (x + 0.1, y + 0.1, 0.77), OCHRE)
b.box((-0.13, -1.5, 0.77), (0.13, 0.35, 0.92), GREEN) # 살미 3(쇠서)
b.box((-0.135, -1.5, 0.77), (0.135, -1.32, 0.92), RED)
return b.build('Gongpo_Dapo_Set')
BRACKET = None
def inst(proto, loc, rot=(0, 0, 0), scale=(1, 1, 1), name=None):
"""숨긴 원형 메시를 공유하는 보이는 복제본."""
ob = duplicate(proto, loc=loc, rot=rot, scale=scale, name=name)
ob.hide_render = False
ob.hide_viewport = False
if 'atelier_cutter' in ob:
del ob['atelier_cutter']
return ob
def floor_frame(name, xs, ys, z_col_top, gongpo_skip=()):
"""창방·평방·포벽·장혀·외목도리 띠와 단청 머리초, 공포 배열. 공포대 윗면 z 반환."""
Wx, Wy = xs[-1], ys[-1]
fr = MB()
zc = z_col_top
zb = zc + 0.22
ring_beams(fr, Wx, Wy, 0, 0.19, zc - 0.5, zc, GREEN) # 창방
ring_beams(fr, Wx, Wy, 0, 0.29, zc, zb, GREEN) # 평방
ring_beams(fr, Wx, Wy, 0, 0.06, zb, zb + 0.74, RED) # 포벽
ring_beams(fr, Wx, Wy, 0, 0.2, zb + 0.74, zb + 1.06, GREEN) # 장혀
ring_beams(fr, Wx, Wy, 1.25, 0.14, zb + 0.9, zb + 1.13, GREEN) # 외목도리
# 머리초(창방·평방 기둥머리 단청)
for (cx, cy) in [(x, s * Wy) for x in xs for s in (-1, 1)]:
fr.box((cx - 0.6, cy - 0.205, zc - 0.48), (cx + 0.6, cy + 0.205, zc - 0.02), BLUE)
fr.box((cx - 0.22, cy - 0.215, zc - 0.47), (cx + 0.22, cy + 0.215, zc - 0.03), RED)
fr.box((cx - 0.5, cy - 0.3, zc + 0.02), (cx + 0.5, cy + 0.3, zb - 0.02), OCHRE)
for (cx, cy) in [(s * Wx, y) for y in ys[1:-1] for s in (-1, 1)]:
fr.box((cx - 0.205, cy - 0.6, zc - 0.48), (cx + 0.205, cy + 0.6, zc - 0.02), BLUE)
fr.box((cx - 0.215, cy - 0.22, zc - 0.47), (cx + 0.215, cy + 0.22, zc - 0.03), RED)
fr.box((cx - 0.3, cy - 0.5, zc + 0.02), (cx + 0.3, cy + 0.5, zb - 0.02), OCHRE)
fr.build(name + '_Beams_Dancheong')
# 공포 배열
n = max(2, round(2 * Wx / 1.3))
for i in range(n + 1):
x = -Wx + 2 * Wx * i / n
for s in (-1, 1):
if s < 0 and abs(x) < 0.9 and 'front_center' in gongpo_skip:
continue
inst(BRACKET, loc=(x, s * Wy, zb), rot=(0, 0, 0 if s < 0 else math.pi), name=name + '_Gongpo')
m = max(2, round(2 * Wy / 1.3))
for i in range(1, m):
y = -Wy + 2 * Wy * i / m
for s in (-1, 1):
inst(BRACKET, loc=(s * Wx, y, zb), rot=(0, 0, s * math.pi / 2), name=name + '_Gongpo')
return zb + 1.13
def main():
global BRACKET
PT = build_platform()
# 하층·상층 기둥 격자
XL = [-11.0, -7.0, -2.6, 2.6, 7.0, 11.0]
YL = [-5.0, 0.0, 5.0]
XU = [-9.4, -6.0, -2.2, 2.2, 6.0, 9.4]
YU = [-3.6, 0.0, 3.6]
ZB = PT + 0.6 # 기단 윗면
ZC1 = ZB + 3.9 # 하층 기둥머리
# 기단
gd = MB()
gd.box((-12.3, -6.3, PT), (12.3, 6.3, ZB), GRANITE)
gd.box((-12.15, -6.15, ZB), (12.15, 6.15, ZB + 0.02), GRANITE2)
for x in XL:
for y in YL:
if abs(y) > 0.1 or abs(x) > 10:
gd.box((x - 0.48, y - 0.48, ZB + 0.02), (x + 0.48, y + 0.48, ZB + 0.3), GRANITE)
gd.build('Gidan_Base_Jucho')
BRACKET = make_bracket()
BRACKET.location = (0, 0, -100)
BRACKET.hide_render = True
BRACKET.hide_viewport = True
BRACKET['atelier_cutter'] = True
colp = column_mesh()
colp.hide_render = True
colp.hide_viewport = True
colp['atelier_cutter'] = True
# 하층 평주
for x in XL:
for y in YL:
if abs(y) > 0.1 or abs(x) > 10:
inst(colp, loc=(x, y, ZB + 0.3), scale=(1, 1, ZC1 - ZB - 0.3), name='Column_Lower')
zbt1 = floor_frame('Lower', XL, YL, ZC1)
# 하층 지붕: 상층 마루 둘레(9.9 x 4.1)에서 잘린 우진각 처마
ax1, ay1 = 11.0 + 3.3, 5.0 + 3.2
tc = 1 - (YU[-1] + 0.5) / ay1
r1 = ax1 - (ax1 - (XU[-1] + 0.5)) / tc
h1 = 6.0
tp1 = 1 - (YL[-1] + 1.25) / ay1
ze1 = zbt1 + 0.36 + 0.17 - h1 * f_prof(tp1)
R1 = Roof(ax1, ay1, r1, h1, ze1, lift=0.75, bulge=0.55)
build_roof('Roof_Lower', R1, tc, tp1, full=False, n_figs=5)
zdeck = ze1 + h1 * f_prof(tc) + 0.03
ZU = zdeck + 0.3
ZC2 = ZU + 2.9
# 상층 마루·고주(하층 바닥부터 올라가는 내진 기둥)
dk = MB()
DX, DY = XU[-1] + 0.95, YU[-1] + 0.95
dk.box((-DX, -DY, zdeck), (DX, DY, ZU), WOOD)
dk.box((-DX - 0.03, -DY - 0.03, zdeck + 0.02), (DX + 0.03, DY + 0.03, zdeck + 0.2), GREEN)
# 계자난간
hr = 0.95
rail_pts = []
nx, ny = 16, 6
for i in range(nx + 1):
for s in (-1, 1):
rail_pts.append((-DX + 0.1 + 2 * (DX - 0.1) * i / nx, s * (DY - 0.1)))
for i in range(1, ny):
for s in (-1, 1):
rail_pts.append((s * (DX - 0.1), -(DY - 0.1) + 2 * (DY - 0.1) * i / ny))
for x, y in rail_pts:
dk.box((x - 0.07, y - 0.07, ZU), (x + 0.07, y + 0.07, ZU + hr + 0.08), COL)
ox, oy = DX - 0.1, DY - 0.1
for z0, z1, hw, m in ((ZU + 0.05, ZU + 0.14, 0.06, COL), (ZU + 0.5, ZU + 0.58, 0.06, COL),
(ZU + hr - 0.06, ZU + hr + 0.04, 0.075, COL), (ZU + 0.14, ZU + 0.5, 0.025, GREEN)):
for s in (-1, 1):
dk.box((-ox - hw, s * oy - hw, z0), (ox + hw, s * oy + hw, z1), m)
dk.box((s * ox - hw, -oy + hw, z0), (s * ox + hw, oy - hw, z1), m)
dk.build('Upper_Floor_Deck_Railing')
for x in XU:
for y in YU:
if abs(y) > 0.1 or abs(x) > 9:
inst(colp, loc=(x, y, ZB + 0.3), scale=(0.95, 0.95, ZC2 - ZB - 0.3), name='Column_Upper_Gozu')
# 상층 판벽
wl = MB()
Wx2, Wy2 = XU[-1], YU[-1]
for s in (-1, 1):
for i in range(len(XU) - 1):
x0, x1 = XU[i] + 0.2, XU[i + 1] - 0.2
wl.box((x0, s * Wy2 - 0.08, ZU), (x1, s * Wy2 + 0.08, ZC2 - 0.5), COL)
wl.box((x0, s * Wy2 - 0.12, ZU), (x1, s * Wy2 + 0.12, ZU + 0.35), GREEN) # 하인방
wl.box((x0, s * Wy2 - 0.12, ZU + 1.55), (x1, s * Wy2 + 0.12, ZU + 1.75), GREEN) # 중인방
# 판문 띠장
nb = 3
for k in range(1, nb + 1):
xx = x0 + (x1 - x0) * k / (nb + 1)
wl.box((xx - 0.05, s * Wy2 - 0.1, ZU + 0.35), (xx + 0.05, s * Wy2 + 0.1, ZU + 1.55), GREEN)
# 상부 창살(교창)
xx = x0 + 0.15
while xx < x1 - 0.15:
wl.box((xx - 0.025, s * Wy2 - 0.11, ZU + 1.75), (xx + 0.025, s * Wy2 + 0.11, ZC2 - 0.5), WOOD)
xx += 0.22
for i in range(len(YU) - 1):
y0, y1 = YU[i] + 0.2, YU[i + 1] - 0.2
wl.box((s * Wx2 - 0.08, y0, ZU), (s * Wx2 + 0.08, y1, ZC2 - 0.5), COL)
wl.box((s * Wx2 - 0.12, y0, ZU), (s * Wx2 + 0.12, y1, ZU + 0.35), GREEN)
wl.box((s * Wx2 - 0.12, y0, ZU + 1.55), (s * Wx2 + 0.12, y1, ZU + 1.75), GREEN)
wl.build('Upper_Panel_Walls')
zbt2 = floor_frame('Upper', XU, YU, ZC2, gongpo_skip=('front_center',))
# 상층 우진각 지붕
ax2, ay2 = Wx2 + 3.2, Wy2 + 3.2
h2 = 5.6
tp2 = 1 - (Wy2 + 1.25) / ay2
ze2 = zbt2 + 0.36 + 0.17 - h2 * f_prof(tp2)
R2 = Roof(ax2, ay2, ax2 - ay2 * 1.02, h2, ze2, lift=0.95, bulge=0.65)
build_roof('Roof_Upper', R2, 0.985, tp2, full=True, n_figs=7)
# 현판(세로로 건 '崇禮門' 현판 — 글자는 획 덩어리로 암시)
pq = MB()
pw, ph_ = 0.72, 1.5
pq.box((-pw, -0.07, -ph_), (pw, 0.07, ph_), GREEN)
pq.box((-pw + 0.1, -0.09, -ph_ + 0.1), (pw - 0.1, 0.0, ph_ - 0.1), GOLD)
pq.box((-pw + 0.15, -0.1, -ph_ + 0.15), (pw - 0.15, 0.0, ph_ - 0.15), PLAQUE)
rnd = random.Random(7)
for c in range(3):
cz = ph_ - 0.14 - (2 * ph_ - 0.28) * (c + 0.5) / 3
for k in range(7):
if rnd.random() < 0.5:
w, hh = rnd.uniform(0.25, 0.5), 0.06
else:
w, hh = 0.06, rnd.uniform(0.2, 0.32)
px = rnd.uniform(-0.38 + w, 0.38 - w)
pz = cz + rnd.uniform(-0.28 + hh, 0.28 - hh)
pq.box((px - w, -0.13, pz - hh), (px + w, -0.08, pz + hh), GOLD)
plq = pq.build('Hyeonpan_Sungnyemun')
plq.location = (0, -(Wy2 + 0.95), ZC2 - 0.45)
plq.rotation_euler = (math.radians(-6), 0, 0)
# 현판 걸이
hk = MB()
hk.box((-0.06, -(Wy2 + 1.0), ZC2 + 0.9), (0.06, -(Wy2 + 0.2), ZC2 + 1.05), COL)
hk.build('Hyeonpan_Hanger')
for proto in (BRACKET, colp):
bpy.data.objects.remove(proto, do_unlink=True)
main()

다른 모형