본문으로 건너뛰기
3D 아틀리에
타지마할 3D 모형

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

이 모형에 대하여

타지마할은 인도 아그라의 야무나강 남쪽 기슭에 있는 영묘다. 무굴 제국의 황제 샤 자한이 1631년 세상을 떠난 왕비 뭄타즈 마할을 기리기 위해 짓게 했으며, 본당은 1640년대에 대부분 완성되었고 정원과 부속 건물을 포함한 전체 단지는 1653년 무렵 마무리되었다. 1983년 유네스코 세계유산으로 등재되었다.

본당은 사방 약 95m의 대리석 기단 위에 놓인 모서리를 깎은 정사각형 건물이다. 네 면이 같은 모양으로, 가운데에 큰 아치 벽감(이완)이 있고 양옆과 깎인 모서리에 2단 아치 벽감이 배치된다. 지붕 중앙에는 높은 드럼 위에 양파형 돔이 얹히고, 그 둘레를 네 개의 작은 정자(차트리)가 둘러싼다. 기단의 네 모서리에는 세 개의 발코니로 나뉜 첨탑이 서 있다. 외벽은 라자스탄 마크라나산 흰 대리석으로 마감하고, 아치 테두리에는 검은 대리석으로 꾸란 구절을 상감했으며 꽃무늬는 색 돌을 박아 넣는 피에트라 두라 기법으로 장식했다.

이 모델은 본당, 기단, 네 첨탑을 실제 크기에 가깝게 재현했다. 정원과 수로, 양옆의 모스크와 영빈관, 대문은 포함하지 않았고, 붉은 사암 테라스는 건물을 받치는 바닥으로만 간략하게 두었다.

만든 방법

중앙 돔, 드럼, 차트리 지붕, 첨탑 몸통은 단면 곡선을 Z축으로 돌린 회전체로 만들었고, 첨탑의 발코니와 난간도 같은 단면 안에 넣어 한 번에 돌렸다. 본당은 모서리를 깎은 팔각 프리즘에 이완 돌출부를 불리언 합집합으로 붙인 뒤, 뾰족 아치 단면을 돌출한 프리즘 수십 개를 불리언 차집합으로 파내 벽감을 만들었다. 아치를 두르는 상감 띠는 아치 곡선을 따라 만든 얇은 U자 띠와 판으로 표현했고, 꽃무늬 상감은 마름모 판 몇 개로 단순화했다. 첨탑, 차트리, 소첨탑은 하나의 메시를 공유하는 복제본으로 배치했다. 내부 공간, 대리석 투각 창(잘리)의 무늬, 첨탑이 바깥쪽으로 살짝 기운 점은 생략했으며, 돔과 드럼의 비례는 사진을 참고한 근사값이라 모델의 전체 높이는 실제보다 몇 미터 높다.

build.py

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

# 타지마할 (인도 아그라, 1632~1653) — Blender Python 코드만으로 만든 모델.
# 붉은 사암 테라스 위 흰 대리석 기단(약 95m 사방), 모서리를 깎은 팔각 본당(약 57m),
# 네 면의 큰 이완(pishtaq) 아치와 2단 벽감, 드럼 위 양파형 돔, 네 개의 차트리, 기단 모서리의 첨탑 네 개.
# 돔·첨탑·차트리 지붕은 단면 곡선을 Z축으로 돌린 회전체(lathe)로, 아치 벽감은 뾰족 아치 단면 프리즘을
# 불리언으로 파냈다. 아치 테두리의 검은 대리석 상감 띠는 얇은 판으로 표현했고, 같은 부품(첨탑·차트리·
# 소첨탑)은 메시를 공유하는 복제본으로 배치했다. 미터 단위 실물 크기 기준이며, 기단 바닥에서 첨두까지 약 78m로
# 실제(약 73m)보다 조금 높게 잡혔다.
from atelier import *
from mathutils import Matrix, Vector
import math
SHOT = {"azimuth": -72, "elevation": 8, "lens": 50, "light": "day", "background": "#26324a"}
# ── 재질 ────────────────────────────────────────────────────────────
M_MARBLE = mat('Makrana marble', '#ebe4d6', 0.32)
M_MARBLE_SHADE = mat('Marble recess', '#d8cfbf', 0.45)
M_INLAY = mat('Black marble inlay', '#3b3631', 0.3)
M_PIETRA = mat('Pietra dura inlay', '#8c6a4c', 0.4)
M_GOLD = mat('Gilded bronze finial', '#c8a04a', 0.28, 1.0)
M_SAND = mat('Red sandstone', '#8c4636', 0.8)
M_JALI = mat('Marble jali screen', '#6d6257', 0.6)
M_DOOR = mat('Dark doorway', '#2e2a27', 0.7)
MATS = [M_MARBLE, M_INLAY, M_GOLD, M_JALI, M_DOOR, M_MARBLE_SHADE, M_PIETRA, M_SAND]
MI = {m: i for i, m in enumerate(MATS)}
# ── 치수 (m) ───────────────────────────────────────────────────────
TER_H = 1.2 # 붉은 사암 테라스
Z0 = TER_H + 6.7 # 대리석 기단 윗면
PL = 95.0 # 기단 한 변
S = 28.5 # 본당 반폭
C = 10.5 # 모서리 깎임
WALL_H = 26.0 # 본당 벽 높이(기단 위)
ROOF = WALL_H + 0.15 # 처마 띠 윗면 = 지붕면
PISH_W, PISH_PJ, PISH_H = 21.0, 1.2, 31.5
CHAMF_D = (2 * S - C) / math.sqrt(2)
def frame(cx, cy, nx, ny, z=0.0):
"""면 좌표계: u=면을 따라(정면에서 볼 때 오른쪽), d=안쪽, z=위."""
tx, ty = -ny, nx
return Matrix(((tx, -nx, 0, cx), (ty, -ny, 0, cy), (0, 0, 1, z), (0, 0, 0, 1)))
def arch_profile(w, z0, hs, k=0.65, n=9, u=0.0):
"""뾰족(이심) 아치 단면 (u, z) 점 목록, 반시계."""
h = w / 2
R = k * w
cx = R - h
나머지 367줄 펼치기
a_top = math.acos(cx / R)
pts = [(u - h, z0), (u + h, z0)]
for i in range(n + 1): # 오른쪽 호: 0 → a_top (중심은 왼쪽)
a = a_top * i / n
pts.append((u - cx + R * math.cos(a), hs + R * math.sin(a)))
for i in range(1, n + 1): # 왼쪽 호: 꼭짓점 → 스프링
a = math.pi - a_top + (a_top) * i / n
pts.append((u + cx + R * math.cos(a), hs + R * math.sin(a)))
# 꼭짓점 중복 제거
out = []
for p in pts:
if not out or (abs(out[-1][0] - p[0]) > 1e-6 or abs(out[-1][1] - p[1]) > 1e-6):
out.append(p)
return out
class Part:
"""여러 재질 면을 한 bmesh 에 모아 하나의 메시 오브젝트로 만든다."""
def __init__(self, name):
self.name = name
self.bm = bmesh.new()
def _tag(self, faces, m, sm):
for f in faces:
f.material_index = MI[m]
f.smooth = sm
def box(self, M, lo, hi, m=M_MARBLE):
c = [(a + b) / 2 for a, b in zip(lo, hi)]
s = [b - a for a, b in zip(lo, hi)]
T = M @ Matrix.Translation(c) @ Matrix.Diagonal((*s, 1))
r = bmesh.ops.create_cube(self.bm, size=1.0, matrix=T)
self._tag({f for v in r['verts'] for f in v.link_faces}, m, False)
def prism(self, pts, z0, z1, M=Matrix(), m=M_MARBLE):
b = [self.bm.verts.new(M @ Vector((x, y, z0))) for x, y in pts]
t = [self.bm.verts.new(M @ Vector((x, y, z1))) for x, y in pts]
fs = [self.bm.faces.new(list(reversed(b))), self.bm.faces.new(t)]
n = len(pts)
for i in range(n):
fs.append(self.bm.faces.new((b[i], b[(i + 1) % n], t[(i + 1) % n], t[i])))
self._tag(fs, m, False)
def lathe(self, prof, M=Matrix(), seg=32, m=M_MARBLE, smooth=True, rot0=0.0):
rings = []
for r, z in prof:
if r < 1e-6:
rings.append([self.bm.verts.new(M @ Vector((0, 0, z)))] * seg)
else:
rings.append([self.bm.verts.new(M @ Vector((r * math.cos(rot0 + 2 * math.pi * i / seg),
r * math.sin(rot0 + 2 * math.pi * i / seg), z)))
for i in range(seg)])
fs = []
for j in range(len(prof) - 1):
a, b = rings[j], rings[j + 1]
for i in range(seg):
i1 = (i + 1) % seg
q = [a[i], a[i1], b[i1], b[i]]
q2 = []
for v in q:
if v not in q2:
q2.append(v)
if len(q2) >= 3:
fs.append(self.bm.faces.new(q2))
self._tag(fs, m, smooth)
def cyl(self, M, r, z0, z1, seg=16, m=M_MARBLE, r2=None):
self.lathe([(0, z0), (r, z0), (r if r2 is None else r2, z1), (0, z1)], M, seg, m, smooth=True)
def arch(self, M, w, z0, hs, d0, d1, k=0.65, u=0.0, m=M_MARBLE):
prof = arch_profile(w, z0, hs, k, u=u)
f_ = [self.bm.verts.new(M @ Vector((x, d0, z))) for x, z in prof]
b_ = [self.bm.verts.new(M @ Vector((x, d1, z))) for x, z in prof]
fs = [self.bm.faces.new(f_), self.bm.faces.new(list(reversed(b_)))]
n = len(prof)
for i in range(n):
fs.append(self.bm.faces.new((f_[i], b_[i], b_[(i + 1) % n], f_[(i + 1) % n])))
bmesh.ops.recalc_face_normals(self.bm, faces=fs)
self._tag(fs, m, False)
def arch_band(self, M, w, z0, hs, b, d0, d1, k=0.65, u=0.0, m=M_INLAY):
"""아치 입구를 두르는 U자 띠(폭 b) — 상감 테두리."""
po = arch_profile(w + 2 * b, z0, hs, k * w / (w + 2 * b) + 0.5 * (1 - w / (w + 2 * b)), u=u)
pi_ = arch_profile(w + 0.06, z0, hs, (k * w + 0.03) / (w + 0.06), u=u)
po, pi_ = po[1:] + po[:1], pi_[1:] + pi_[:1] # 오른쪽 밑 → 호 → 왼쪽 밑
V = lambda p, d: self.bm.verts.new(M @ Vector((p[0], d, p[1])))
rows = [[V(a, d0), V(c, d0), V(c, d1), V(a, d1)] for a, c in zip(po, pi_)]
fs = []
for r0, r1 in zip(rows[:-1], rows[1:]):
for i in range(4):
fs.append(self.bm.faces.new((r0[i], r0[(i + 1) % 4], r1[(i + 1) % 4], r1[i])))
fs.append(self.bm.faces.new(rows[0]))
fs.append(self.bm.faces.new(list(reversed(rows[-1]))))
bmesh.ops.recalc_face_normals(self.bm, faces=fs)
self._tag(fs, m, False)
def build(self, loc=(0, 0, 0)):
for e in self.bm.edges: # 30° 넘는 모서리는 각지게
if len(e.link_faces) == 2 and e.calc_face_angle(0) > math.radians(30):
e.smooth = False
me = bpy.data.meshes.new(self.name)
self.bm.to_mesh(me)
self.bm.free()
used = sorted({p.material_index for p in me.polygons})
for m in MATS:
me.materials.append(m)
ob = bpy.data.objects.new(self.name, me)
link(ob)
ob.location = loc
return ob
ONION = [(0.86, 0.0), (0.93, 0.05), (0.975, 0.11), (0.997, 0.18), (1.0, 0.25), (0.99, 0.32), (0.96, 0.40),
(0.905, 0.48), (0.83, 0.56), (0.73, 0.64), (0.61, 0.71), (0.48, 0.775), (0.35, 0.835), (0.23, 0.885),
(0.14, 0.925), (0.085, 0.955), (0.05, 0.98), (0.0, 1.0)]
def onion(h, rmax, rbase=None):
"""양파형 돔 단면 (r, z): 아래 40% 에서 가장 부풀고 위로 오목하게 뾰족해진다."""
return [(0.0, 0.0)] + [(r * rmax, z * h) for r, z in ONION]
# ── 붉은 사암 테라스와 대리석 기단 ──────────────────────────────────────
base = Part('Plinth_and_Terrace')
base.box(Matrix(), (-57, -57, 0), (57, 57, TER_H), M_SAND)
base.box(Matrix(), (-58, -58, -0.01), (58, 58, 0.35), M_SAND) # 테라스 하단 턱
base.box(Matrix(), (-PL / 2, -PL / 2, TER_H - 0.05), (PL / 2, PL / 2, Z0 - 0.35), M_MARBLE)
base.box(Matrix(), (-PL / 2 - 0.35, -PL / 2 - 0.35, Z0 - 0.6), (PL / 2 + 0.35, PL / 2 + 0.35, Z0), M_MARBLE)
for i in range(-10, 11): # 기단 윗면 대리석 포장 줄눈
g = i * 4.5
base.box(Matrix(), (g - 0.09, -PL / 2 + 0.1, Z0 - 0.05), (g + 0.09, PL / 2 - 0.1, Z0 + 0.025), M_MARBLE_SHADE)
base.box(Matrix(), (-PL / 2 + 0.1, g - 0.09, Z0 - 0.05), (PL / 2 - 0.1, g + 0.09, Z0 + 0.03), M_MARBLE_SHADE)
plinth = base.build()
# 기단 측면의 블라인드 아케이드(불리언으로 파냄)
pc = Part('cut_plinth_arcade')
N_ARC = 17
for side in range(4):
a = side * math.pi / 2
nx, ny = math.sin(a) * 1, -math.cos(a)
M = frame(nx * PL / 2, ny * PL / 2, nx, ny, TER_H)
step = PL / N_ARC
for i in range(N_ARC):
u = -PL / 2 + step * (i + 0.5)
if abs(u) > PL / 2 - step: # 첨탑 받침 자리 비움
continue
pc.arch(M, 3.4, 0.9, 3.4, -1.0, 0.55, u=u)
cut_pl = pc.build()
boolean(plinth, cut_pl)
plinth.modifiers[-1].use_self = True
apply_all(plinth)
bpy.data.objects.remove(cut_pl, do_unlink=True)
# ── 본당: 팔각 몸체 + 이완 돌출부, 아치 벽감을 파냄 ────────────────────────
oct_pts = [(S - C, -S), (S, -(S - C)), (S, S - C), (S - C, S), (-(S - C), S), (-S, S - C), (-S, -(S - C)), (-(S - C), -S)]
body_p = Part('Mausoleum_Body')
body_p.prism(oct_pts, -0.05, WALL_H)
body = body_p.build(loc=(0, 0, Z0))
pish = Part('cut_pishtaq_union')
cut = Part('cut_alcoves')
FACES = [] # (frame matrix, kind)
for side in range(4):
a = side * math.pi / 2
nx, ny = math.sin(a), -math.cos(a)
FACES.append((frame(nx * S, ny * S, nx, ny), 'main'))
for side in range(4):
a = side * math.pi / 2 + math.pi / 4
nx, ny = math.sin(a), -math.cos(a)
FACES.append((frame(nx * CHAMF_D, ny * CHAMF_D, nx, ny), 'chamfer'))
for M, kind in FACES:
if kind == 'main':
pish.box(M, (-PISH_W / 2, -PISH_PJ, -0.05), (PISH_W / 2, 3.0, PISH_H))
cut.arch(M, 13.6, -0.6, 15.6, -PISH_PJ - 1.0, 4.2) # 큰 이완 아치
cut.arch(M, 5.0, -0.6, 5.6, 4.0, 5.4) # 안쪽 출입문
cut.arch(M, 5.0, 10.2, 14.6, 4.0, 5.0) # 안쪽 위층 창
for uu in (-14.25, 14.25): # 양옆 2단 벽감
cut.arch(M, 5.0, 1.0, 8.0, -1.0, 2.2, u=uu)
cut.arch(M, 5.0, 13.6, 20.2, -1.0, 2.2, u=uu)
else:
cut.arch(M, 8.6, 1.0, 7.4, -1.0, 3.0, k=0.62)
cut.arch(M, 8.6, 13.6, 19.4, -1.0, 3.0, k=0.62)
pish_ob = pish.build(loc=(0, 0, Z0))
cut_ob = cut.build(loc=(0, 0, Z0))
boolean(body, pish_ob, 'UNION')
boolean(body, cut_ob, 'DIFFERENCE')
body.modifiers[-1].use_self = True
apply_all(body)
bpy.data.objects.remove(pish_ob, do_unlink=True)
bpy.data.objects.remove(cut_ob, do_unlink=True)
# ── 상감 띠, 처마, 난간, 창살, 문 ───────────────────────────────────────
trim = Part('Mausoleum_Trim')
P = 0.08 # 상감 띠 돌출
for M, kind in FACES:
if kind == 'main':
f = -PISH_PJ
# 이완 아치를 두르는 사각 캘리그래피 띠
ox, zt, bw = 8.4, 26.6, 0.7
trim.box(M, (-ox, f - P, 0.0), (-ox + bw, f + 0.05, zt), M_INLAY)
trim.box(M, (ox - bw, f - P, 0.0), (ox, f + 0.05, zt), M_INLAY)
trim.box(M, (-ox + bw, f - P, zt - bw), (ox - bw, f + 0.05, zt), M_INLAY)
for sx in (-1, 1): # 스팬드럴 피에트라 두라 꽃무늬
R45 = M @ Matrix.Translation((sx * 6.55, f - 0.04, 23.6)) @ Matrix.Rotation(math.radians(45), 4, 'Y')
trim.box(R45, (-0.6, -0.04, -0.6), (0.6, 0.09, 0.6), M_PIETRA)
trim.box(R45, (-0.3, -0.07, -0.3), (0.3, 0.0, 0.3), M_INLAY)
# 이완 바깥 테두리(밝은 띠)
trim.box(M, (-PISH_W / 2, f - P / 2, PISH_H - 1.0), (PISH_W / 2, f + 0.05, PISH_H - 0.6), M_INLAY)
# 이완 위 난간
trim.box(M, (-PISH_W / 2 - 0.1, f - 0.15, PISH_H - 0.1), (PISH_W / 2 + 0.1, f + 3.0, PISH_H + 0.9))
trim.arch_band(M, 13.6, 0.0, 15.6, 0.45, f - P, f + 0.05)
trim.arch_band(M, 5.0, 0.0, 5.6, 0.25, 4.0 - P, 4.05, m=M_PIETRA)
trim.arch_band(M, 5.0, 10.2, 14.6, 0.25, 4.0 - P, 4.05, m=M_PIETRA)
# 안쪽 문, 창살
trim.arch(M, 4.6, -0.05, 5.6, 5.25, 5.5, m=M_DOOR)
trim.arch(M, 4.6, 10.4, 14.6, 4.85, 5.1, m=M_JALI)
for uu in (-14.25, 14.25):
for z0, z1 in ((0.5, 13.0), (13.1, 25.0)):
for x0, x1 in ((uu - 3.2, uu - 2.9), (uu + 2.9, uu + 3.2)):
trim.box(M, (x0, -P, z0), (x1, 0.05, z1), M_INLAY)
trim.box(M, (uu - 2.9, -P, z1 - 0.3), (uu + 2.9, 0.05, z1), M_INLAY)
trim.arch_band(M, 5.0, 1.0, 8.0, 0.22, -P, 0.05, u=uu)
trim.arch_band(M, 5.0, 13.6, 20.2, 0.22, -P, 0.05, u=uu)
trim.arch(M, 4.0, 1.0, 8.0, 2.05, 2.3, u=uu, m=M_JALI)
trim.arch(M, 4.0, 13.6, 20.2, 2.05, 2.3, u=uu, m=M_JALI)
else:
for z0, z1 in ((0.5, 13.0), (13.1, 25.0)):
trim.box(M, (-5.2, -P, z0), (-4.9, 0.05, z1), M_INLAY)
trim.box(M, (4.9, -P, z0), (5.2, 0.05, z1), M_INLAY)
trim.box(M, (-4.9, -P, z1 - 0.3), (4.9, 0.05, z1), M_INLAY)
trim.arch_band(M, 8.6, 1.0, 7.4, 0.3, -P, 0.05, k=0.62)
trim.arch_band(M, 8.6, 13.6, 19.4, 0.3, -P, 0.05, k=0.62)
trim.arch(M, 7.6, 1.0, 7.4, 2.85, 3.1, k=0.62, m=M_JALI)
trim.arch(M, 7.6, 13.6, 19.4, 2.85, 3.1, k=0.62, m=M_JALI)
# 처마 띠(지붕판)와 난간
out_pts = [(x * 1.012, y * 1.012) for x, y in oct_pts]
trim.prism(out_pts, WALL_H - 0.75, ROOF)
for i in range(8):
x0, y0 = oct_pts[i]
x1, y1 = oct_pts[(i + 1) % 8]
L = math.hypot(x1 - x0, y1 - y0)
nx, ny = (y1 - y0) / L, -(x1 - x0) / L
M = frame((x0 + x1) / 2, (y0 + y1) / 2, nx, ny)
trim.box(M, (-L / 2, -0.25, ROOF - 0.05), (L / 2, 0.5, ROOF + 1.1))
trim_ob = trim.build(loc=(0, 0, Z0))
# ── 소첨탑(굴다스타): 팔각 모서리와 이완 양옆 ─────────────────────────────
pin = Part('Guldasta_Pinnacle')
pin.cyl(Matrix(), 0.8, 0.0, 1.0, 16)
pin.cyl(Matrix(), 0.65, 1.0, 29.0, 16)
pin.lathe([(0, 29.0), (0.95, 29.0), (0.95, 29.35), (0.6, 29.5), (0.6, 30.6), (0.9, 30.6), (0.9, 30.85), (0, 30.85)], seg=16)
pin.lathe([(0, 30.8), (0.6, 30.8), (0.75, 31.3), (0.6, 31.9), (0.25, 32.4), (0, 32.7)], seg=16)
pin.cyl(Matrix(), 0.08, 32.5, 33.6, 8, M_GOLD, r2=0.02)
pin_ob = pin.build(loc=(0, 0, Z0))
pins = []
for i, (x, y) in enumerate(oct_pts):
pins.append(pin_ob if i == 0 else duplicate(pin_ob, loc=(x, y, Z0)))
pin_ob.location = (oct_pts[0][0], oct_pts[0][1], Z0)
for M, kind in FACES:
if kind != 'main':
continue
for uu in (-PISH_W / 2, PISH_W / 2):
p = M @ Vector((uu, -PISH_PJ, 0))
duplicate(pin_ob, loc=(p.x, p.y, Z0 + 0.0), scale=(1, 1, (PISH_H + 3.5) / 31.0))
# ── 중앙 드럼과 양파형 돔, 금빛 첨두 ─────────────────────────────────────
dome = Part('Main_Dome')
zr = ROOF
DR = 11.6 # 드럼 반경
dome.lathe([(0, zr - 0.2), (13.2, zr - 0.2), (13.2, zr + 1.4), (12.5, zr + 1.4), (12.5, zr + 1.9),
(DR, zr + 1.9), (DR, zr + 10.6), (DR + 0.5, zr + 10.7), (DR + 0.5, zr + 11.5), (0, zr + 11.5)], seg=72)
for k in range(2):
dome.lathe([(DR - 0.02, zr + 2.6 + k * 7.4), (DR + 0.12, zr + 2.6 + k * 7.4), (DR + 0.12, zr + 2.95 + k * 7.4), (DR - 0.02, zr + 2.95 + k * 7.4)],
seg=72, m=M_INLAY)
ZD = zr + 11.4
DH = 24.5
dome.lathe([(r, ZD + z) for r, z in onion(DH, 13.3)], seg=80)
# 연꽃 왕관
dome.lathe([(0, ZD + DH * 0.86), (3.6, ZD + DH * 0.86), (3.9, ZD + DH * 0.88), (2.6, ZD + DH * 0.925), (1.3, ZD + DH * 0.975), (0, ZD + DH * 0.985)], seg=32)
N_DR = 20
for i in range(N_DR): # 드럼의 블라인드 아치 테두리
a = 2 * math.pi * (i + 0.5) / N_DR
M = frame(DR * math.cos(a), DR * math.sin(a), math.cos(a), math.sin(a), zr)
dome.arch_band(M, 2.1, 3.4, 7.2, 0.16, -0.1, 0.12)
dome_ob = dome.build(loc=(0, 0, Z0))
fin = Part('Dome_Finial')
fz = ZD + DH - 0.4
fin.lathe([(0, fz), (1.0, fz), (1.0, fz + 0.4), (0.55, fz + 0.8), (1.25, fz + 1.8), (0.55, fz + 2.8), (0.95, fz + 3.5),
(0.4, fz + 4.2), (0.75, fz + 4.9), (0.3, fz + 5.6), (0.28, fz + 7.6), (0.14, fz + 9.0), (0, fz + 9.3)], seg=24, m=M_GOLD)
fin_ob = fin.build(loc=(0, 0, Z0))
# ── 차트리(작은 정자) 네 개 ────────────────────────────────────────────
def chhatri(name, s=1.0, cols=8):
ch = Part(name)
ch.lathe([(0, -0.2 * s), (4.6 * s, -0.2 * s), (4.6 * s, 2.0 * s), (0, 2.0 * s)], seg=8, smooth=False, rot0=math.pi / 8)
ch.lathe([(0, 1.9 * s), (4.85 * s, 1.9 * s), (4.85 * s, 2.25 * s), (0, 2.25 * s)], seg=8, smooth=False, rot0=math.pi / 8)
for i in range(cols):
a = 2 * math.pi * i / cols
M = Matrix.Translation((3.75 * s * math.cos(a), 3.75 * s * math.sin(a), 0))
ch.lathe([(0, 2.2 * s), (0.42 * s, 2.2 * s), (0.42 * s, 2.7 * s), (0.3 * s, 2.9 * s), (0.3 * s, 6.1 * s),
(0.42 * s, 6.3 * s), (0.42 * s, 6.6 * s), (0, 6.6 * s)], M, seg=10)
ch.lathe([(0, 6.5 * s), (4.25 * s, 6.5 * s), (4.25 * s, 7.4 * s), (0, 7.4 * s)], seg=8, smooth=False, rot0=math.pi / 8)
ch.lathe([(0, 7.3 * s), (5.3 * s, 7.3 * s), (5.3 * s, 7.55 * s), (3.9 * s, 8.1 * s), (0, 8.1 * s)], seg=8, smooth=False, rot0=math.pi / 8)
ch.lathe([(0, 8.0 * s), (3.2 * s, 8.0 * s), (3.2 * s, 8.9 * s), (0, 8.9 * s)], seg=32)
ch.lathe([(r * s * 0.3, 8.85 * s + z * s * 0.3) for r, z in onion(15.0, 12.0)], seg=32)
ch.lathe([(0, 13.0 * s), (0.45 * s, 13.0 * s), (0.25 * s, 13.4 * s), (0.4 * s, 13.8 * s), (0.12 * s, 14.4 * s),
(0.05 * s, 15.6 * s), (0, 15.8 * s)], seg=12, m=M_GOLD)
return ch
ch_ob = chhatri('Chhatri_Kiosk').build()
RC = 22.5 / math.sqrt(2)
for i, (sx, sy) in enumerate(((1, 1), (-1, 1), (-1, -1), (1, -1))):
loc = (sx * RC, sy * RC, Z0 + ROOF)
if i == 0:
ch_ob.location = loc
else:
duplicate(ch_ob, loc=loc)
# ── 첨탑 네 개 ───────────────────────────────────────────────────────
mn = Part('Minaret')
mn.lathe([(0, -0.05), (4.3, -0.05), (4.3, 1.4), (4.0, 1.6), (0, 1.6)], seg=8, smooth=False, rot0=math.pi / 8)
def balcony(z, r):
return [(r, z), (r + 0.35, z + 0.5), (r + 0.95, z + 1.1), (r + 1.2, z + 1.25), (r + 1.2, z + 1.55),
(r + 1.05, z + 1.55), (r + 1.05, z + 2.6), (r + 0.85, z + 2.6), (r + 0.85, z + 1.7), (r - 0.1, z + 1.7)]
shaft = [(0, 1.5), (2.7, 1.5)]
segs = [(1.5, 11.5, 2.7, 2.55), (13.2, 22.0, 2.45, 2.32), (23.7, 31.0, 2.22, 2.1)]
for zi, (z0, z1, r0, r1) in enumerate(segs):
shaft += [(r0, z0), (r1, z1)]
shaft += balcony(z1, r1)
shaft += [(0, 32.7)]
mn.lathe(shaft, seg=32)
for z0, z1, r0, r1 in segs: # 첨탑 상감 고리
for t in (0.2, 0.55, 0.9):
z = z0 + (z1 - z0) * t
r = r0 + (r1 - r0) * t + 0.03
mn.lathe([(r, z), (r, z + 0.25)], seg=32, m=M_INLAY)
# 꼭대기 차트리
s = 0.62
TOPZ = 32.6
for i in range(8):
a = 2 * math.pi * i / 8
M = Matrix.Translation((2.0 * math.cos(a), 2.0 * math.sin(a), TOPZ))
mn.lathe([(0, 0), (0.28, 0), (0.22, 0.3), (0.22, 3.4), (0.3, 3.6), (0, 3.6)], M, seg=8)
mn.lathe([(0, TOPZ + 3.5), (2.45, TOPZ + 3.5), (2.45, TOPZ + 4.0), (0, TOPZ + 4.0)], seg=8, smooth=False, rot0=math.pi / 8)
mn.lathe([(0, TOPZ + 3.95), (3.1, TOPZ + 3.95), (3.1, TOPZ + 4.15), (2.2, TOPZ + 4.6), (0, TOPZ + 4.6)], seg=16, smooth=False)
mn.lathe([(r * 0.19, TOPZ + 4.5 + z * 0.19) for r, z in onion(15.0, 12.0)], seg=32)
mn.lathe([(0, TOPZ + 7.1), (0.3, TOPZ + 7.1), (0.15, TOPZ + 7.5), (0.25, TOPZ + 7.8), (0.05, TOPZ + 9.0), (0, TOPZ + 9.1)], seg=10, m=M_GOLD)
mn_ob = mn.build()
MP = PL / 2 - 4.4
for i, (sx, sy) in enumerate(((1, 1), (-1, 1), (-1, -1), (1, -1))):
loc = (sx * MP, sy * MP, Z0)
if i == 0:
mn_ob.location = loc
mn_ob.name = 'Minaret_NE'
else:
duplicate(mn_ob, loc=loc, name=('Minaret_NE', 'Minaret_NW', 'Minaret_SW', 'Minaret_SE')[i])

다른 모형