본문으로 건너뛰기
3D 아틀리에
피사의 사탑 3D 모형

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

이 모형에 대하여

피사의 사탑은 이탈리아 피사 기적의 광장(Piazza dei Miracoli)에 있는 대성당의 종탑이다. 1173년에 공사를 시작했으나 3층까지 올렸을 때 무른 점토 지반 때문에 기울기 시작했고, 전쟁 등으로 공사가 여러 차례 중단된 끝에 14세기 후반에 종루가 완성되었다. 늦게 쌓은 위층은 기울기를 보정하려고 한쪽을 더 높게 쌓아, 탑 전체가 바나나처럼 미세하게 휘어 있다.

탑은 흰색과 회색 대리석으로 지은 8층 원통형 구조다. 1층은 붙임기둥과 블라인드 아치로 벽을 장식하고, 2층부터 7층까지는 가는 기둥이 받치는 아치 회랑이 탑을 한 바퀴 두른다. 꼭대기의 종루는 지름이 더 작으며 일곱 개의 종을 단다. 기울기는 20세기 말 5.5°에 가까워졌으나 1990~2001년의 지반 보강 공사로 약 4°까지 줄었다.

이 모델은 높이 약 56m, 기단 지름 약 15.5m의 실제 치수로 탑을 재현했다. 1층의 블라인드 아치와 마름모 상감, 층마다 30개의 기둥이 이어지는 여섯 단의 회랑, 처마 돌림띠, 꼭대기 테라스 난간과 아치 너머로 보이는 청동 종을 표현했고, 탑 전체를 남쪽으로 3.97° 기울였다.

만든 방법

기단·처마 돌림띠·종루 지붕·종은 (r, z) 단면을 Z 축으로 돌린 회전체로 만들었다. 아치 사이의 벽(스팬드럴)은 '사각형에서 반원을 뺀' 평면 형상을 원통 좌표에 감아 bmesh 로 직접 짰고, 아치를 따라 도는 테(아키볼트)도 같은 방식으로 붙였다. 기둥은 주춧돌·주초·배흘림 기둥몸·주두를 한 메시로 합친 뒤 linked 복제로 회랑 180개와 1층 15개를 배치했다. 모든 탑 부품을 하나의 빈 오브젝트에 묶어 3.97° 기울이고, 종루만 0.8° 되돌려 위층의 기울기 보정을 단순하게 흉내 냈다. 대리석의 미세한 결과 풍화 얼룩, 회랑 안쪽 벽의 실제 문과 장식, 내부 나선 계단은 생략하고 회색 줄눈 띠와 어두운 문 개구부로 대신했다.

build.py

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

# 피사의 사탑 (Torre di Pisa) — Blender Python 절차적 모델
# 높이 약 56m, 기단 지름 약 15.5m 의 대리석 원통 종탑을 실제 치수로 만들었다.
# 1층은 15개의 붙임기둥과 블라인드 아치(마름모 상감), 2~7층은 기둥 30개짜리 회랑 6단,
# 맨 위는 지름이 작은 종루(아치 개구부 12개와 청동 종)다. 아치 사이 벽(스팬드럴)은
# 원통 좌표로 bmesh 를 직접 짜서 휘어진 아치 띠를 만들고, 기둥·아치는 linked 복제로 메시를 공유한다.
# 처마 돌림띠와 기단은 회전체(lathe)로 만들었다. 탑 전체를 남쪽(-Y)으로 약 4° 기울였고,
# 공사 중 기울기를 바로잡으려 한 흔적처럼 종루는 0.8° 덜 기울게 했다. 바닥에는 기적의 광장 잔디와 포석을 깔았다.
import math
from atelier import *
SHOT = {"azimuth": -8, "elevation": 9, "lens": 70, "light": "day", "background": "#24303f"}
# ── 재질 ─────────────────────────────────────────────
M_MARBLE = mat('CarraraMarble', '#e4dccb', rough=0.55)
M_COLUMN = mat('ColumnMarble', '#ece5d6', rough=0.45)
M_WALL = mat('GalleryWallMarble', '#b8ad97', rough=0.6)
M_GREY = mat('GreyStoneBand', '#9a968c', rough=0.6)
M_INLAY = mat('GreenInlay', '#6e7c70', rough=0.5)
M_DARK = mat('DoorwayShadow', '#3b342c', rough=0.8)
M_BRONZE = mat('BellBronze', '#8c6b3c', rough=0.38, metal=0.85)
M_WOOD = mat('BellYokeOak', '#5a4129', rough=0.7)
M_IRON = mat('WroughtIron', '#3a3c3e', rough=0.5, metal=0.6)
M_LAWN = mat('PiazzaLawn', '#58733a', rough=0.92)
M_PAVE = mat('Paving', '#cbbfa8', rough=0.8)
M_ROOF = mat('LeadRoof', '#7b7e80', rough=0.5, metal=0.3)
LEAN = math.radians(3.97)
lean = bpy.data.objects.new('PisaTower_Lean', None)
link(lean)
lean.rotation_euler = (LEAN, 0, 0)
belfry_pivot = bpy.data.objects.new('Belfry_Correction', None)
link(belfry_pivot)
belfry_pivot.parent = lean
def own(ob, parent=None):
ob.parent = parent or lean
return ob
# ── 메시 도우미 ──────────────────────────────────────
def sector(name, r0, r1, a0, a1, z0, z1, seg=4, material=None):
"""원통 좌표의 휘어진 블록(r0~r1, 각도 a0~a1, 높이 z0~z1)."""
bm = bmesh.new()
rows = []
for i in range(seg + 1):
나머지 300줄 펼치기
a = a0 + (a1 - a0) * i / seg
c, s = math.cos(a), math.sin(a)
rows.append([bm.verts.new((r * c, r * s, z)) for r, z in ((r0, z0), (r1, z0), (r1, z1), (r0, z1))])
for i in range(seg):
for j in range(4):
bm.faces.new((rows[i][j], rows[i + 1][j], rows[i + 1][(j + 1) % 4], rows[i][(j + 1) % 4]))
bm.faces.new(rows[0])
bm.faces.new(list(reversed(rows[-1])))
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
link(ob)
return assign(ob, material)
def arch_bay(name, rref, r_in, r_out, half, ra, z_s, z_top, n=14, material=None):
"""아치 하나 분의 휘어진 스팬드럴 벽. u(호 길이)·z 평면의 '사각형 - 반원'을 원통에 감는다."""
H = z_top - z_s
tc = math.atan2(H, half)
ths = sorted(set([math.pi * i / n for i in range(n + 1)] + [tc, math.pi - tc]), reverse=True)
arc, bnd = [], []
for t in ths:
c, s = math.cos(t), math.sin(t)
arc.append((ra * c, z_s + ra * s))
tt = min(half / abs(c) if abs(c) > 1e-9 else 1e9, H / s if s > 1e-9 else 1e9)
bnd.append((tt * c, z_s + tt * s))
bm = bmesh.new()
def V(u, z, r):
p = u / rref
return bm.verts.new((r * math.cos(p), r * math.sin(p), z))
ao = [V(u, z, r_out) for u, z in arc]
ai = [V(u, z, r_in) for u, z in arc]
bo = [V(u, z, r_out) for u, z in bnd]
bi = [V(u, z, r_in) for u, z in bnd]
m = len(ths)
for k in range(m - 1):
bm.faces.new((ao[k], ao[k + 1], bo[k + 1], bo[k])) # 바깥면
bm.faces.new((ai[k], bi[k], bi[k + 1], ai[k + 1])) # 안쪽면
bm.faces.new((ao[k], ai[k], ai[k + 1], ao[k + 1])) # 아치 안쪽(내륜)
if abs(bnd[k][1] - z_top) < 1e-6 and abs(bnd[k + 1][1] - z_top) < 1e-6:
bm.faces.new((bo[k], bo[k + 1], bi[k + 1], bi[k])) # 윗면
for k in (0, m - 1): # 기둥머리 위 받침면
bm.faces.new((ao[k], bo[k], bi[k], ai[k]))
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
link(ob)
return assign(ob, material)
def archivolt(name, rref, r0, r1, ra0, ra1, z_s, n=16, material=None):
"""아치 둘레를 따라 바깥으로 살짝 돋운 반원 테(아키볼트)."""
bm = bmesh.new()
rows = []
for i in range(n + 1):
t = math.pi * i / n
row = []
for r, ra in ((r0, ra0), (r1, ra0), (r1, ra1), (r0, ra1)):
p = ra * math.cos(t) / rref
row.append(bm.verts.new((r * math.cos(p), r * math.sin(p), z_s + ra * math.sin(t))))
rows.append(row)
for i in range(n):
for j in range(4):
bm.faces.new((rows[i][j], rows[i + 1][j], rows[i + 1][(j + 1) % 4], rows[i][(j + 1) % 4]))
bm.faces.new(rows[0])
bm.faces.new(list(reversed(rows[-1])))
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
link(ob)
return assign(ob, material)
def ring_profile(name, pts, material, seg=96):
"""닫힌 (r,z) 단면을 회전시킨 돌림띠."""
ob = lathe(name, pts + [pts[0]], material=material, seg=seg, shade_smooth=False)
return ob
def column(name, h, rs, material, plinth=True, cap_scale=1.0):
"""로마네스크 기둥: 사각 주춧돌 + 아틱 주초 + 약간 배흘림 기둥몸 + 주두 + 사각 관판."""
parts = []
pl = 0.12 * cap_scale
if plinth:
parts.append(box(name + '_plinth', (rs * 2.7, rs * 2.7, pl), material=material, base=True))
zb = pl if plinth else 0.0
cap_h = 0.42 * cap_scale
ab = 0.12 * cap_scale
shaft_top = h - cap_h - ab
prof = [(0, zb), (rs * 1.32, zb), (rs * 1.32, zb + 0.06 * cap_scale), (rs * 1.18, zb + 0.12 * cap_scale),
(rs * 1.25, zb + 0.18 * cap_scale), (rs * 1.05, zb + 0.24 * cap_scale), (rs, zb + 0.3 * cap_scale),
(rs * 1.03, zb + (shaft_top - zb) * 0.35), (rs * 0.9, shaft_top - 0.05), (rs * 1.0, shaft_top - 0.03),
(rs * 1.0, shaft_top), (rs * 0.95, shaft_top + 0.02), (rs * 1.15, shaft_top + cap_h * 0.45),
(rs * 1.38, shaft_top + cap_h * 0.85), (rs * 1.42, shaft_top + cap_h), (0, shaft_top + cap_h)]
lt = lathe(name + '_shaft', prof, material=material, seg=14)
parts.append(lt)
parts.append(box(name + '_abacus', (rs * 3.0, rs * 3.0, ab), loc=(0, 0, h - ab), material=material, base=True))
return join(parts, name)
def place_ring(proto, count, radius, z=0.0, start=0.0, parent=None, name=None):
obs = []
for i in range(count):
a = start + 2 * math.pi * i / count
ob = proto if i == 0 else duplicate(proto, name=f'{name or proto.name}.{i:02d}')
ob.location = (radius * math.cos(a), radius * math.sin(a), z)
ob.rotation_euler = (0, 0, a)
own(ob, parent)
obs.append(ob)
return obs
def place_rot(proto, count, start=0.0, parent=None, name=None):
"""원점 기준으로 이미 반경에 놓인 부품을 Z 축 회전으로 복제."""
for i in range(count):
ob = proto if i == 0 else duplicate(proto, name=f'{name or proto.name}.{i:02d}')
ob.rotation_euler = (0, 0, start + 2 * math.pi * i / count)
own(ob, parent)
# ── 치수 ─────────────────────────────────────────────
Z_GROUND_TOP = 11.3 # 1층 처마(첫 돌림띠 윗면)
H_FLOOR = 6.0 # 회랑층 층고
N_LOGGIA = 6
R_CORE = 5.5 # 회랑 안쪽 원통벽
R_COL = 7.15 # 회랑 기둥 중심 반경
R_CORN = 7.62 # 돌림띠 바깥 반경
CORN_DN, CORN_UP = 0.6, 0.15
# ── 기단 · 1층 ───────────────────────────────────────
base = ring_profile('Tower_BaseMoulding', [(0.0, 0.0), (7.95, 0.0), (7.95, 0.55), (7.85, 0.7), (7.75, 0.78),
(7.75, 0.95), (7.6, 1.05), (0.0, 1.05)], M_MARBLE)
own(base)
wall0 = cylinder('GroundFloor_Wall', r=7.35, h=Z_GROUND_TOP - CORN_DN - 1.0 + 0.05, loc=(0, 0, 1.0), seg=96,
material=M_MARBLE)
own(wall0)
for k, zb in enumerate((2.6, 4.4, 6.2, 8.0)):
own(cylinder(f'GroundFloor_GreyCourse_{k}', r=7.37, h=0.22, loc=(0, 0, zb), seg=96, material=M_GREY))
N0 = 15
B0 = 2 * math.pi / N0
R0C = 7.47
col0 = column('GroundFloor_HalfColumn', 8.1, 0.3, M_COLUMN, cap_scale=1.4)
place_ring(col0, N0, R0C, z=1.05, name='GroundFloor_HalfColumn')
half0 = R0C * B0 / 2
bay0 = arch_bay('GroundFloor_BlindArch', R0C, 7.25, 7.72, half0, half0 - 0.36, 1.05 + 8.1, Z_GROUND_TOP - CORN_DN,
material=M_MARBLE)
bay0 = join([bay0, archivolt('GroundFloor_Archivolt', R0C, 7.70, 7.78, half0 - 0.36, half0 - 0.18, 1.05 + 8.1,
material=M_MARBLE)], 'GroundFloor_BlindArch')
place_rot(bay0, N0, start=B0 / 2, name='GroundFloor_BlindArch')
# 블라인드 아치 안의 마름모 상감(로산게)
loz = prism('GroundFloor_Lozenge', [(0, -0.42), (0.55, 0), (0, 0.42), (-0.55, 0)], 0.06, material=M_INLAY)
loz.data.transform(Matrix.Rotation(math.pi / 2, 4, 'Y')) # 돌출 방향을 +X(반경 바깥)로
loz.data.transform(Matrix.Translation((7.33, 0, 0)))
DOOR_BAY = 14 # 카메라 쪽(-12°) 아치에 정문
for i in range(N0):
a = B0 / 2 + B0 * i
if i == DOOR_BAY:
continue
ob = loz if i == 0 else duplicate(loz, name=f'GroundFloor_Lozenge.{i:02d}')
ob.location = (0, 0, 9.75)
ob.rotation_euler = (0, 0, a)
own(ob)
# 정문: 문틀(대리석) + 어두운 문 개구부 + 위쪽 반달 상인방
a_d = B0 / 2 + B0 * DOOR_BAY
door = join([
sector('MainDoor_JambL', 7.33, 7.52, -0.13, -0.095, 1.05, 4.75, seg=1, material=M_MARBLE),
sector('MainDoor_JambR', 7.33, 7.52, 0.095, 0.13, 1.05, 4.75, seg=1, material=M_MARBLE),
sector('MainDoor_Lintel', 7.33, 7.56, -0.15, 0.15, 4.75, 5.25, seg=3, material=M_MARBLE),
], 'MainDoor_Frame')
door.rotation_euler = (0, 0, a_d)
own(door)
door_open = sector('MainDoor_Opening', 7.34, 7.42, -0.095, 0.095, 1.05, 4.75, seg=3, material=M_DARK)
door_open.rotation_euler = (0, 0, a_d)
own(door_open)
door_tymp = prism('MainDoor_Lunette', [(math.cos(t) * 0.8, math.sin(t) * 0.8) for t in
[math.pi * i / 12 for i in range(13)]], 0.06, material=M_INLAY)
door_tymp.data.transform(Matrix.Rotation(math.pi / 2, 4, 'X'))
door_tymp.data.transform(Matrix.Rotation(math.pi / 2, 4, 'Z'))
door_tymp.data.transform(Matrix.Translation((7.33, 0, 5.25)))
door_tymp.rotation_euler = (0, 0, a_d)
own(door_tymp)
# ── 돌림띠(처마) — 층마다 하나, 회랑 바닥과 천장을 겸함 ──
corn_prof = lambda zt: [(R_CORE - 0.2, zt - CORN_DN), (R_CORN - 0.2, zt - CORN_DN), (R_CORN - 0.05, zt - CORN_DN + 0.08),
(R_CORN - 0.05, zt - 0.32), (R_CORN + 0.1, zt - 0.24), (R_CORN + 0.1, zt - 0.1),
(R_CORN, zt), (R_CORN - 0.32, zt), (R_CORN - 0.36, zt + CORN_UP),
(R_CORE - 0.2, zt + CORN_UP)]
for k in range(N_LOGGIA + 1):
zt = Z_GROUND_TOP + k * H_FLOOR
own(ring_profile(f'Cornice_{k + 1}', corn_prof(zt), M_MARBLE))
# ── 회랑층 2~7 ───────────────────────────────────────
NL = 30
BL = 2 * math.pi / NL
COL_H = 3.95
lcol = column('Loggia_Column', COL_H, 0.19, M_COLUMN)
halfL = R_COL * BL / 2
lbay = arch_bay('Loggia_Arch', R_COL, 6.88, 7.42, halfL, halfL - 0.2, CORN_UP + COL_H,
H_FLOOR - CORN_DN, material=M_MARBLE)
lbay = join([lbay, archivolt('Loggia_Archivolt', R_COL, 7.40, 7.46, halfL - 0.2, halfL - 0.1, CORN_UP + COL_H,
material=M_MARBLE)], 'Loggia_Arch')
core_door = join([
sector('Loggia_CoreDoor', R_CORE - 0.05, R_CORE + 0.03, -0.09, 0.09, 0.0, 2.3, seg=2, material=M_DARK),
], 'Loggia_CoreDoor')
for f in range(N_LOGGIA):
z0 = Z_GROUND_TOP + f * H_FLOOR
fl = f + 2
core = cylinder(f'Floor{fl}_GalleryWall', r=R_CORE, h=H_FLOOR, loc=(0, 0, z0), seg=64, material=M_WALL, cap=False)
own(core)
for b, zz in enumerate((1.6, 3.4)):
own(cylinder(f'Floor{fl}_GreyCourse_{b}', r=R_CORE + 0.02, h=0.2, loc=(0, 0, z0 + CORN_UP + zz), seg=64,
cap=False, material=M_GREY))
for c in range(NL):
a = BL * c
ob = lcol if (f == 0 and c == 0) else duplicate(lcol, name=f'Floor{fl}_Column.{c:02d}')
ob.location = (R_COL * math.cos(a), R_COL * math.sin(a), z0 + CORN_UP)
ob.rotation_euler = (0, 0, a)
own(ob)
ob = lbay if (f == 0 and c == 0) else duplicate(lbay, name=f'Floor{fl}_Arch.{c:02d}')
ob.location = (0, 0, z0)
ob.rotation_euler = (0, 0, a + BL / 2)
own(ob)
for d in range(5):
ob = core_door if (f == 0 and d == 0) else duplicate(core_door, name=f'Floor{fl}_CoreDoor.{d}')
ob.location = (0, 0, z0 + CORN_UP)
ob.rotation_euler = (0, 0, BL / 2 + d * 2 * math.pi / 5 + f * 0.7)
own(ob)
# ── 7층 위 테라스 난간 ───────────────────────────────
Z_TERR = Z_GROUND_TOP + N_LOGGIA * H_FLOOR + CORN_UP
post = box('Terrace_RailPost', (0.05, 0.05, 1.0), material=M_IRON, base=True)
place_ring(post, 72, 7.12, z=Z_TERR, name='Terrace_RailPost')
for k, zz in enumerate((0.45, 0.98)):
own(torus(f'Terrace_Rail_{k}', R=7.12, r=0.03, loc=(0, 0, Z_TERR + zz), seg=96, rseg=6,
material=M_IRON))
# ── 종루 ─────────────────────────────────────────────
own(cylinder('Terrace_Floor', r=5.6, h=0.4, loc=(0, 0, Z_TERR - 0.41), seg=64, material=M_MARBLE))
belfry_pivot.location = (0, 0, Z_TERR)
belfry_pivot.rotation_euler = (math.radians(-0.8), 0, 0)
BP = belfry_pivot
RB_O, RB_I = 4.55, 3.85
NB = 12
BB = 2 * math.pi / NB
own(ring_profile('Belfry_Plinth', [(0.0, -0.25), (4.85, -0.25), (4.85, 0.35), (4.7, 0.45), (4.6, 0.6), (4.6, 1.1),
(0.0, 1.1)], M_MARBLE), BP)
own(cylinder('Belfry_Floor', r=RB_I + 0.05, h=0.02, loc=(0, 0, 1.1), seg=48, material=M_WALL), BP)
Z_SPRING_B = 5.0
pier = sector('Belfry_Pier', RB_I, RB_O, -0.15, 0.15, 1.1, Z_SPRING_B, seg=2, material=M_MARBLE)
place_rot(pier, NB, parent=BP, name='Belfry_Pier')
bcol = column('Belfry_HalfColumn', Z_SPRING_B - 1.1, 0.17, M_COLUMN)
place_ring(bcol, NB, RB_O + 0.02, z=1.1, parent=BP, name='Belfry_HalfColumn')
halfB = (RB_O + 0.02) * BB / 2
bbay = arch_bay('Belfry_Arch', RB_O + 0.02, RB_I, RB_O, halfB, halfB - 0.62, Z_SPRING_B, 6.5, material=M_MARBLE)
place_rot(bbay, NB, start=BB / 2, parent=BP, name='Belfry_Arch')
own(ring_profile('Belfry_Cornice', [(RB_I - 0.3, 6.5), (RB_O + 0.05, 6.5), (RB_O + 0.25, 6.7), (RB_O + 0.25, 6.95),
(RB_O + 0.05, 7.05), (RB_O, 7.05), (RB_O, 7.75), (RB_O + 0.15, 7.85),
(RB_O + 0.15, 8.0), (RB_O - 0.35, 8.0), (RB_O - 0.35, 7.25),
(RB_I - 0.3, 7.25)], M_MARBLE), BP)
own(lathe('Belfry_Roof', [(RB_O - 0.35, 7.3), (2.0, 7.55), (0.9, 7.75), (0.9, 8.6), (0.0, 8.75)],
material=M_ROOF, seg=48), BP)
own(cylinder('Belfry_Ceiling', r=RB_I + 0.05, h=0.2, loc=(0, 0, 6.3), seg=48, material=M_WALL), BP)
# 종(청동) — 아치 개구부 안쪽에 매달린 7개
bell = lathe('Bell', [(0.0, -0.95), (0.62, -0.95), (0.6, -0.86), (0.5, -0.75), (0.42, -0.45), (0.38, -0.1),
(0.3, 0.05), (0.0, 0.1)], material=M_BRONZE, seg=28)
yoke = box('Bell_Yoke', (0.25, 1.1, 0.25), loc=(0, 0, 0.12), material=M_WOOD, base=True)
hang = box('Bell_Hanger', (0.12, 0.12, 6.3 - 3.9 - 0.37), material=M_WOOD, base=True)
for i, k in enumerate((0, 2, 4, 6, 7, 9, 11)):
a = BB / 2 + BB * k
r = 2.9
loc = (r * math.cos(a), r * math.sin(a), 3.9)
b = bell if i == 0 else duplicate(bell, name=f'Bell.{i}')
b.location = loc
own(b, BP)
y = yoke if i == 0 else duplicate(yoke, name=f'Bell_Yoke.{i}')
y.location = loc
y.rotation_euler = (0, 0, a)
own(y, BP)
h = hang if i == 0 else duplicate(hang, name=f'Bell_Hanger.{i}')
h.location = (loc[0], loc[1], 3.9 + 0.37)
own(h, BP)
# ── 바닥: 기단 계단 · 포석 · 잔디 (기울지 않음) ──────
lawn = box('PiazzaDeiMiracoli_Lawn', (26, 26, 0.6), loc=(0, -0.8, -0.6), material=M_LAWN, base=True)
pave = cylinder('Paving_Ring', r=11.5, h=0.64, loc=(0, -0.5, -0.6), seg=96, material=M_PAVE)
step1 = cylinder('Tower_Step_1', r=9.4, h=0.35 + 0.6 - 0.02, loc=(0, 0, -0.58), seg=96, material=M_MARBLE)
step2 = cylinder('Tower_Step_2', r=8.7, h=0.75 + 0.6 - 0.04, loc=(0, 0, -0.56), seg=96, material=M_MARBLE)

다른 모형