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

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

이 모형에 대하여

첨성대는 경상북도 경주시 인왕동, 신라의 옛 왕궁터 가까이에 서 있는 석조 건축물이다. 『삼국유사』에 선덕여왕 대에 돌을 다듬어 쌓았다는 기록이 있어 7세기 중엽에 세워진 것으로 본다. 하늘을 관측하던 시설로 널리 알려져 있으며, 동아시아에 남아 있는 가장 오래된 천문대 유적으로 꼽힌다. 국보로 지정되어 있다.

네모난 2단 기단 위에 다듬은 화강암을 27단으로 둥글게 쌓았다. 몸통은 아래가 넓고 위로 갈수록 오목하게 좁아지다 윗부분에서 거의 곧게 서는 병 모양이며, 높이는 약 9.17m이다. 남쪽 중간 높이에는 네모난 창구가 하나 뚫려 있고, 안쪽은 창구 아래까지 흙으로 채워져 있다. 위쪽 두 곳에는 안을 가로지르는 긴 돌의 끝이 바깥으로 드러나 있고, 꼭대기에는 긴 돌을 井 자로 짜 맞춘 정자석이 두 단 얹혀 있다.

이 모델은 몸통 석재를 단마다 한 개씩 깎아 엇갈려 쌓은 모습, 남쪽 창구와 그 안의 흙 채움, 돌출된 장대석 끝, 2단 정자석과 꼭대기 동쪽 절반을 덮은 판석, 2단 기단을 실제 크기로 표현했다.

만든 방법

몸통 바깥선은 아래 반지름 2.465m에서 위 반지름 1.425m로 줄어드는 거듭제곱 곡선으로 정했다. 각 단은 원호 모양 석재를 bmesh로 하나씩 만들어(모서리 모따기, 면 가운데를 약간 볼록하게) 단마다 경계를 엇갈리게 배치했고, 석재 폭과 돌출량은 고정 시드의 난수로 조금씩 달리했다. 창구는 13~15단에서 해당 각도의 석재를 비워 만들었고, 정자석은 동서 장대석 사이로 남북 장대석을 나누어 끼워 맞물린 모양을 냈다. 석재 하나하나의 실제 크기와 개수, 표면의 풍화 얼룩, 몸통의 미세한 기울어짐은 재현하지 않았으며 석재 색은 화강암 네 가지 톤을 섞어 단순화했다.

build.py

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

# 첨성대 (경주, 신라 7세기) — 코드로만 쌓은 화강암 27단 석탑
#
# - 몸통: 아래 지름 약 4.93m, 위 지름 약 2.85m 의 병 모양 곡선 프로필을 수식으로 정하고,
# 27개 단마다 둥근 원호 모양 석재를 bmesh 로 한 개씩 깎아(모서리 모따기·면 볼록) 엇갈려 쌓았다.
# - 남쪽 13~15단에 네모난 창구를 비우고, 안쪽은 12단 높이까지 흙으로 채웠다.
# - 19~20단, 25~26단에는 안을 가로지르는 장대석의 끝이 바깥으로 튀어나온다.
# - 꼭대기에는 장대석 넷을 井 자로 맞물린 정자석 2단과 동쪽 절반을 덮은 판석을 얹었다.
# - 아래에는 네모난 2단 기단을 판석들로 둘렀다. 석재 색은 세 가지 화강암 톤을 섞었다.
from atelier import *
import random
random.seed(31)
SHOT = {"azimuth": -76, "elevation": 13, "lens": 60, "light": "day", "background": "#2a3442"}
# ---------------------------------------------------------------- 재질
GRANITE = [
mat('Granite_Light', '#a39a88', rough=0.88),
mat('Granite_Warm', '#928a7a', rough=0.9),
mat('Granite_Grey', '#8b8579', rough=0.9),
mat('Granite_Weathered', '#76716a', rough=0.95),
]
M_BASE = mat('Granite_Plinth', '#8e897e', rough=0.92)
M_CROWN = mat('Granite_Crown', '#938b7c', rough=0.88)
M_SOIL = mat('Packed_Earth', '#5a4936', rough=1.0)
# ---------------------------------------------------------------- 치수
BASE_LOW = (5.36, 0.20) # 하층 기단 한 변, 높이
BASE_UP = (5.10, 0.19) # 상층 기단
Z0 = BASE_LOW[1] + BASE_UP[1]
N_COURSE = 27
CH = 0.30 # 한 단 높이
R_BOT, R_TOP = 2.465, 1.425
TOP_Z = Z0 + N_COURSE * CH
WIN_COURSES = (12, 13, 14) # 0부터 센 13~15단
WIN_HALF = 0.48 # 창구 반폭
WIN_ANGLE = -math.pi / 2 # 남쪽(-Y)
def r_out(z):
"""몸통 바깥 반지름: 아래가 넓고 오목하게 좁아지다 위에서 살짝 벌어지는 병 모양."""
t = max(0.0, min(1.0, (z - Z0) / (N_COURSE * CH)))
return R_TOP + (R_BOT - R_TOP) * (1 - t) ** 1.95 + 0.04 * t ** 8
def wall(z):
t = (z - Z0) / (N_COURSE * CH)
return 0.46 - 0.13 * t
나머지 182줄 펼치기
def r_in(z):
return r_out(z) - wall(z)
# ---------------------------------------------------------------- 몸통 석재
def add_block(bm, a0, a1, z0, z1, mi, c=0.02):
"""원호 석재 하나: 바깥면은 모따기 + 살짝 볼록, 안쪽 면·옆면으로 닫힌 덩어리."""
rm = (r_out(z0) + r_out(z1)) / 2
da = c / rm
n_mid = max(2, int((a1 - a0) * rm / 0.13))
angs = [a0, a0 + da] + [a0 + da + (a1 - a0 - 2 * da) * i / n_mid for i in range(1, n_mid)] + [a1 - da, a1]
zs = [z0, z0 + c, (z0 + z1) / 2, z1 - c, z1]
bulge = random.uniform(0.006, 0.022)
push = random.uniform(-0.012, 0.016)
na, nz = len(angs), len(zs)
outer = []
for j, z in enumerate(zs):
row = []
for i, a in enumerate(angs):
edge = i in (0, na - 1) or j in (0, nz - 1)
r = r_out(z) + push
if edge:
r -= c
else:
u = (a - a0) / (a1 - a0)
v = (z - z0) / (z1 - z0)
r += bulge * math.sin(math.pi * u) * math.sin(math.pi * v)
row.append(bm.verts.new((r * math.cos(a), r * math.sin(a), z)))
outer.append(row)
inner = []
for z in (z0, z1):
ri = r_in(z)
inner.append([bm.verts.new((ri * math.cos(a), ri * math.sin(a), z)) for a in angs])
faces = []
for j in range(nz - 1):
for i in range(na - 1):
f = bm.faces.new((outer[j][i], outer[j][i + 1], outer[j + 1][i + 1], outer[j + 1][i]))
f.smooth = 0 < i < na - 2 and 0 < j < nz - 2
faces.append(f)
for i in range(na - 1):
faces.append(bm.faces.new((inner[0][i + 1], inner[0][i], inner[1][i], inner[1][i + 1])))
faces.append(bm.faces.new((outer[0][i + 1], outer[0][i], inner[0][i], inner[0][i + 1])))
faces.append(bm.faces.new((outer[-1][i], outer[-1][i + 1], inner[1][i + 1], inner[1][i])))
left = [outer[j][0] for j in range(nz)] + [inner[1][0], inner[0][0]]
right = [outer[j][-1] for j in range(nz)] + [inner[1][-1], inner[0][-1]]
faces.append(bm.faces.new(list(reversed(left))))
faces.append(bm.faces.new(right))
for f in faces:
f.material_index = mi
def course_cuts(k, r):
"""k 단의 석재 경계 각도 목록(엇갈림 + 약간의 폭 변화)."""
n = max(12, round(2 * math.pi * r / 0.7))
step = 2 * math.pi / n
start = (k % 2) * 0.5 * step + random.uniform(-0.1, 0.1) * step
cuts = [start + i * step + (random.uniform(-0.28, 0.28) * step if i else 0) for i in range(n)]
return cuts, step
def build_course(k):
z0, z1 = Z0 + k * CH, Z0 + (k + 1) * CH
rm = (r_out(z0) + r_out(z1)) / 2
cuts, step = course_cuts(k, rm)
spans = [(cuts[i], cuts[(i + 1) % len(cuts)] + (2 * math.pi if i == len(cuts) - 1 else 0)) for i in range(len(cuts))]
if k in WIN_COURSES:
rw = r_out(Z0 + 13.5 * CH)
ha = math.asin(WIN_HALF / rw)
lo, hi = WIN_ANGLE - ha, WIN_ANGLE + ha
# 창구 양옆에 경계를 강제하고 그 사이는 비운다
lo2 = lo + 2 * math.pi * math.floor((spans[0][0] - lo) / (2 * math.pi) + 1)
hi2 = lo2 + 2 * ha
new = []
a = hi2
end = lo2 + 2 * math.pi
n = max(1, round((end - a) / step))
for i in range(n):
new.append((a + (end - a) * i / n, a + (end - a) * (i + 1) / n))
spans = new
bm = bmesh.new()
for a0, a1 in spans:
add_block(bm, a0, a1, z0, z1, random.choice((0, 0, 0, 1, 1, 1, 2, 2, 3)))
me = bpy.data.meshes.new(f'Course_{k + 1:02d}')
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(f'Course_{k + 1:02d}', me)
link(ob)
for m in GRANITE:
me.materials.append(m)
return ob
courses = [build_course(k) for k in range(N_COURSE)]
# ---------------------------------------------------------------- 내부 흙 채움(12단까지)
z_fill = Z0 + 12 * CH
fill = cylinder('Interior_Earth_Fill', r=r_in(z_fill) + 0.03, h=0.5, loc=(0, 0, z_fill - 0.5), seg=48, material=M_SOIL)
# 창구 아래턱 판석
sill_r = r_out(z_fill)
sill = box('Window_Sill_Stone', (2 * WIN_HALF + 0.02, wall(z_fill) + 0.04, 0.05),
loc=(0, -(sill_r - wall(z_fill) / 2 + 0.01), z_fill - 0.025 + 0.0), material=M_CROWN)
bevel(sill, 0.01, 1)
sill.location.z = z_fill - 0.024
# ---------------------------------------------------------------- 장대석(끝이 밖으로 나옴)
beam_proto = {}
def cross_beam(name, k, axis, off):
zc = Z0 + (k + 0.5) * CH
L = 2 * (max(r_out(zc - CH / 2), r_out(zc + CH / 2)) + 0.11)
size = (L, 0.30, CH - 0.012) if axis == 'X' else (0.30, L, CH - 0.012)
loc = (0, off, zc) if axis == 'X' else (off, 0, zc)
b = box(name, size, loc=loc, material=M_CROWN)
bevel(b, 0.02, 2)
return b
for k, axis in ((18, 'X'), (19, 'Y'), (24, 'X'), (25, 'Y')):
for s, off in (('A', -0.55), ('B', 0.55)):
cross_beam(f'Cross_Beam_C{k + 1}_{axis}{s}', k, axis, off)
# ---------------------------------------------------------------- 정자석 2단 + 판석
CW, CHH, CL = 0.32, 0.33, 3.10
CA = 1.22
def crown_tier(t):
z = TOP_Z + t * CHH
parts = []
for s in (-1, 1):
b = box(f'Jeongjaseok_T{t + 1}_EW_{"S" if s < 0 else "N"}', (CL, CW, CHH), loc=(0, s * CA, z), material=M_CROWN, base=True)
parts.append(b)
# 남북 방향 장대석은 동서 장대석 사이사이로 나눠 맞물린 것처럼 만든다
segs = [(-CL / 2, -CA - CW / 2), (-CA + CW / 2, CA - CW / 2), (CA + CW / 2, CL / 2)]
for s in (-1, 1):
pieces = []
for y0, y1 in segs:
pieces.append(box('tmp', (CW, y1 - y0, CHH), loc=(s * CA, (y0 + y1) / 2, z), material=M_CROWN, base=True))
parts.append(join(pieces, f'Jeongjaseok_T{t + 1}_NS_{"W" if s < 0 else "E"}'))
for p in parts:
bevel(p, 0.022, 2)
return parts
crown = crown_tier(0) + crown_tier(1)
inner_edge = CA - CW / 2
slab = box('Top_Half_Cover_Slab', (inner_edge - 0.02, 2 * inner_edge - 0.01, 0.16),
loc=((inner_edge + 0.02) / 2 + 0.0, 0, TOP_Z + 0.003), material=M_CROWN, base=True)
bevel(slab, 0.015, 1)
# ---------------------------------------------------------------- 2단 기단
def plinth_tier(name, side, h, z, depth, n_seg):
out = []
hs = side / 2
idx = 0
# 남북 긴 변(전체 길이), 동서 변(안쪽 길이)
for s in (-1, 1):
cuts = sorted([-hs, hs] + [-hs + side * (i + random.uniform(-0.25, 0.25)) / n_seg for i in range(1, n_seg)])
for a, b in zip(cuts, cuts[1:]):
idx += 1
out.append(box(f'{name}_Slab_{idx:02d}', (b - a, depth, h), loc=((a + b) / 2, s * (hs - depth / 2), z), material=M_BASE, base=True))
for s in (-1, 1):
inner = hs - depth
cuts = sorted([-inner, inner] + [-inner + 2 * inner * (i + random.uniform(-0.2, 0.2)) / (n_seg - 1) for i in range(1, n_seg - 1)])
for a, b in zip(cuts, cuts[1:]):
idx += 1
out.append(box(f'{name}_Slab_{idx:02d}', (depth, b - a, h), loc=(s * (hs - depth / 2), (a + b) / 2, z), material=M_BASE, base=True))
core = box(f'{name}_Core', (side - 2 * depth + 0.002, side - 2 * depth + 0.002, h - 0.01), loc=(0, 0, z), material=M_BASE, base=True)
for o in out:
bevel(o, 0.018, 2)
return out + [core]
plinth_tier('Plinth_Lower', BASE_LOW[0], BASE_LOW[1], 0.0, 1.05, 4)
plinth_tier('Plinth_Upper', BASE_UP[0], BASE_UP[1], BASE_LOW[1], 0.95, 4)

다른 모형