본문으로 건너뛰기
3D 아틀리에
그랑디디에바오바브 3D 모형

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

이 모형에 대하여

그랑디디에바오바브는 마다가스카르 서부에서만 자라는 바오바브로, 마다가스카르에 자생하는 바오바브 가운데 가장 크다. 이름은 19세기 프랑스의 박물학자 알프레드 그랑디디에에게서 따왔다. 모론다바 근처의 '바오바브 거리'에 늘어선 나무들이 이 종이다.

줄기는 굵은 원기둥처럼 곧게 서고, 붉은빛이 도는 회색 수피는 매끈하다. 줄기에는 물을 저장하는 조직이 많아 긴 건기를 견딘다. 가지는 줄기 꼭대기에만 모여 거의 수평으로 뻗은 뒤 위로 굽어, 멀리서 보면 납작한 왕관을 얹은 모습이 된다. 잎이 진 건기에는 이 가지가 뿌리처럼 보여 '거꾸로 선 나무'라고도 불린다.

이 모델은 높이 약 26m, 밑동 지름 약 3.2m 의 성목을 실제 크기로 만들었다. 밑동의 뿌리 버팀과 붉은 흙먼지, 줄기의 세로 골과 가로 테 자국, 위쪽의 부러진 옛 가지 자국, 꼭대기에서 갈라지는 3단 가지와 손바닥 모양 겹잎 뭉치, 긴 꼭지에 매달린 열매를 표현했다.

만든 방법

줄기는 높이와 방위각마다 반지름을 계산하는 회전체 격자(96×150)를 bmesh 로 직접 짰고, 세로 골·비스듬한 주름·가로 테 자국·밑동 버팀은 사인 함수의 합으로 반지름에 새겼다. 같은 함수 값으로 면마다 재질(골, 테 자국, 흙먼지, 위쪽 매끈한 수피)을 나눠 텍스처 이미지 없이 무늬를 냈다. 가지는 평행 이동 프레임으로 굵기가 줄어드는 관을 만드는 함수를 써서 1차 12개, 2차, 3차 가지를 재귀적으로 키웠고, 각 가지는 부모 가지 속에서 시작해 이음새가 드러나지 않게 했다. 잎 뭉치 3종과 열매는 메시를 공유하는 복제로 배치했다. 실제 수피의 미세한 결과 잎 하나하나의 모양은 단순화했고, 잎은 우기의 성긴 잎을 표현한 정도로 줄였다.

build.py

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

# 그랑디디에바오바브(Adansonia grandidieri) — 마다가스카르 모론다바 '바오바브 거리'의 그 나무, 높이 약 26m.
# 줄기는 높이·방위각마다 반지름을 계산하는 회전체 격자를 bmesh 로 직접 짰다. 세로 골, 비스듬히 도는 주름,
# 가로 테 자국, 밑동의 뿌리 버팀을 사인 함수의 합으로 새기고, 면마다 재질(먼지 낀 밑동·골·매끈한 수피)을 나눴다.
# 가지는 '꼭대기에만 모여 납작한 왕관을 이루는' 특징을 살려 줄기 끝에서 바깥으로 1차 가지 9개, 위로 3개를 뻗고, 재귀적으로 2·3차
# 가지를 나눴다. 가지는 평행 이동 프레임으로 굵기가 줄어드는 관(tube)을 만들어 부모 가지 속에서 시작한다.
# 잔가지 끝에는 납작한 잎 뭉치를, 2차 가지에는 긴 꼭지에 매달린 열매를 같은 메시를 공유하는 복제로 달았다.
import random
from atelier import *
random.seed(20261023)
SHOT = {"azimuth": -42, "elevation": 9, "lens": 55, "light": "golden", "background": "#2a3040"}
# ---------------------------------------------------------------- 재질
M_BARK = mat('Bark_ReddishGrey', '#8d7d76', rough=0.85)
M_BARK_DARK = mat('Bark_Furrow', '#66574f', rough=0.9)
M_BARK_PALE = mat('Bark_Upper_Smooth', '#a0918a', rough=0.75)
M_DUST = mat('Bark_LateriteDust', '#7f6457', rough=0.95)
M_SCAR = mat('Bark_RingScar', '#776a64', rough=0.9)
M_TWIG = mat('Twig_Grey', '#857265', rough=0.85)
M_LEAF = mat('Leaf_Green', '#5f7d36', rough=0.7)
M_LEAF2 = mat('Leaf_DarkGreen', '#46622b', rough=0.7)
M_FRUIT = mat('Fruit_VelvetPod', '#8c7650', rough=0.9)
# ---------------------------------------------------------------- 치수
H_TRUNK = 19.4 # 줄기 꼭대기(가지가 갈라지는 곳)
R_BASE = 1.55
def trunk_center(z):
# 아주 약한 굽음
return Vector((0.18 * math.sin(z / 7.0), 0.12 * math.sin(z / 5.0 + 1.0), 0.0))
def trunk_radius(z, th):
# 기본 윤곽: 밑동이 넓고, 몸통은 거의 원기둥, 위로 살짝 가늘어지다 꼭대기에서 둥글게 모인다
r = 1.62 - 0.016 * z
r += 0.06 * math.sin(z / 19.4 * math.pi * 1.1) # 가운데 살짝 불룩
r += 0.42 * math.exp(-z / 0.7) # 밑동 퍼짐
# 뿌리 버팀(밑동에만)
but = max(0.0, math.cos(5 * th + 0.4)) ** 3 + 0.6 * max(0.0, math.cos(3 * th + 2.0)) ** 4
r += 0.38 * but * math.exp(-z / 0.55)
# 세로 골과 비스듬한 주름
ridge = 0.030 * math.sin(9 * th + 0.25 * z) + 0.018 * math.sin(17 * th - 0.4 * z + 1.3) \
+ 0.012 * math.sin(29 * th + 0.9 * z)
r *= 1.0 + ridge
# 가로 테 자국
r *= 1.0 - 0.010 * scar_value(z, th)
나머지 295줄 펼치기
# 꼭대기: 둥글게 좁아짐
if z > 16.0:
t = (z - 16.0) / (H_TRUNK - 16.0)
r *= math.sqrt(max(0.0, 1.0 - 0.72 * t ** 1.6))
return r
def scar_value(z, th):
# 드문드문 끊긴 가로 테 자국
band = max(0.0, math.sin(z * 1.35 + 0.6 * math.sin(th * 2 + z))) ** 10
patch = 0.5 + 0.5 * math.sin(3 * th + 1.7 * z)
return band * patch
def ridge_value(z, th):
return math.sin(9 * th + 0.25 * z) + 0.6 * math.sin(17 * th - 0.4 * z + 1.3)
def build_trunk():
seg, nz = 96, 150
bm = bmesh.new()
rings = []
zs = [H_TRUNK * (i / (nz - 1)) ** 1.0 for i in range(nz)]
for z in zs:
c = trunk_center(z)
ring = []
for j in range(seg):
th = 2 * math.pi * j / seg
r = trunk_radius(z, th)
ring.append(bm.verts.new((c.x + r * math.cos(th), c.y + r * math.sin(th), z)))
rings.append(ring)
faces = []
for i in range(nz - 1):
for j in range(seg):
f = bm.faces.new((rings[i][j], rings[i][(j + 1) % seg], rings[i + 1][(j + 1) % seg], rings[i + 1][j]))
zc = (zs[i] + zs[i + 1]) / 2
th = 2 * math.pi * (j + 0.5) / seg
rv = ridge_value(zc, th)
if zc < 0.55 + 0.25 * math.sin(th * 4):
f.material_index = 3
elif scar_value(zc, th) > 0.55:
f.material_index = 4
elif rv < -1.15:
f.material_index = 1
elif zc > 16.6 + 0.9 * math.sin(3 * th + 0.8) + 0.4 * math.sin(7 * th):
f.material_index = 2
else:
f.material_index = 0
faces.append(f)
# 바닥 뚜껑
bm.faces.new(list(reversed(rings[0])))
# 꼭대기 뚜껑(가지가 덮는다)
top_c = bm.verts.new(trunk_center(H_TRUNK) + Vector((0, 0, H_TRUNK + 0.25)))
for j in range(seg):
f = bm.faces.new((rings[-1][j], rings[-1][(j + 1) % seg], top_c))
f.material_index = 2
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
me = bpy.data.meshes.new('Baobab_Trunk')
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new('Baobab_Trunk', me)
link(ob)
for m in (M_BARK, M_BARK_DARK, M_BARK_PALE, M_DUST, M_SCAR):
me.materials.append(m)
smooth(ob)
return ob
# ---------------------------------------------------------------- 가지(관)
def tube(bm, pts, radii, seg, cap_end=True):
n = len(pts)
tans = []
for i in range(n):
a = pts[max(i - 1, 0)]
b = pts[min(i + 1, n - 1)]
tans.append((b - a).normalized())
ref = Vector((0, 0, 1)) if abs(tans[0].z) < 0.9 else Vector((1, 0, 0))
nrm = tans[0].cross(ref).normalized()
rings = []
for i in range(n):
if i > 0:
# 평행 이동
axis = tans[i - 1].cross(tans[i])
if axis.length > 1e-6:
ang = tans[i - 1].angle(tans[i])
nrm = (Matrix.Rotation(ang, 3, axis.normalized()) @ nrm).normalized()
bin_ = tans[i].cross(nrm).normalized()
ring = []
for j in range(seg):
a = 2 * math.pi * j / seg
off = (nrm * math.cos(a) + bin_ * math.sin(a)) * radii[i]
ring.append(bm.verts.new(pts[i] + off))
rings.append(ring)
for i in range(n - 1):
for j in range(seg):
f = bm.faces.new((rings[i][j], rings[i][(j + 1) % seg], rings[i + 1][(j + 1) % seg], rings[i + 1][j]))
f.smooth = True
bm.faces.new(list(reversed(rings[0]))).smooth = True
if cap_end:
tip = bm.verts.new(pts[-1] + tans[-1] * radii[-1] * 0.8)
for j in range(seg):
bm.faces.new((rings[-1][j], rings[-1][(j + 1) % seg], tip)).smooth = True
def grow_path(p0, d, length, steps, bend_up, jitter):
pts = [p0.copy()]
dirs = [d.normalized()]
step = length / steps
cur = d.normalized()
p = p0.copy()
for i in range(steps):
j = Vector((random.uniform(-1, 1), random.uniform(-1, 1), random.uniform(-0.5, 0.5))) * jitter
cur = (cur + Vector((0, 0, bend_up)) + j).normalized()
p = p + cur * step
pts.append(p.copy())
dirs.append(cur.copy())
return pts, dirs
def taper(r0, r1, n, power=1.0):
return [r0 + (r1 - r0) * (i / (n - 1)) ** power for i in range(n)]
def lumpy(radii):
# 마디마다 약간 부풀고 잘록한 굵기 변화
ph = random.uniform(0, 6.28)
n = len(radii)
return [r * (1.0 + 0.07 * math.sin(i * 1.9 + ph)) if 0 < i < n - 1 else r for i, r in enumerate(radii)]
def dir_from(az, el):
return Vector((math.cos(el) * math.cos(az), math.cos(el) * math.sin(az), math.sin(el)))
bm_primary = bmesh.new()
bm_secondary = bmesh.new()
bm_twig = bmesh.new()
tips = [] # 잎 뭉치를 달 잔가지 끝 (위치, 방향)
fruit_spots = [] # 열매를 매달 지점
def spawn_children(pts, dirs, radii, level, count, t_range):
n = len(pts)
for k in range(count):
t = t_range[0] + (t_range[1] - t_range[0]) * (k + random.uniform(0.0, 0.8)) / count
idx = min(n - 2, max(1, int(t * (n - 1))))
base = pts[idx]
pd = dirs[idx]
side = Vector((0, 0, 1)).cross(pd)
if side.length < 1e-4:
side = Vector((1, 0, 0))
side.normalize()
sgn = 1 if k % 2 == 0 else -1
ang = math.radians(random.uniform(28, 50)) * sgn
d = (Matrix.Rotation(ang, 3, Vector((0, 0, 1))) @ pd)
d.z += random.uniform(0.12, 0.4)
d.normalize()
pr = radii[idx]
start = base - d * pr * 0.6
if level == 2:
L = random.uniform(2.4, 3.6)
steps = 7
p2, d2 = grow_path(start, d, L, steps, 0.045, 0.14)
r2 = lumpy(taper(pr * 0.62, 0.045, len(p2), 0.9))
tube(bm_secondary, p2, r2, 10)
spawn_children(p2, d2, r2, 3, random.randint(2, 3), (0.35, 0.95))
fruit_spots.append((p2[len(p2) // 2], r2[len(p2) // 2]))
tips.append((p2[-1], d2[-1]))
else:
L = random.uniform(0.9, 1.6)
steps = 4
p3, d3 = grow_path(start, d, L, steps, 0.10, 0.18)
r3 = taper(min(pr * 0.65, 0.07), 0.018, len(p3))
tube(bm_twig, p3, r3, 6)
tips.append((p3[-1], d3[-1]))
def build_crown():
n_main = 9
for i in range(n_main):
az = 2 * math.pi * i / n_main + random.uniform(-0.2, 0.2)
el = math.radians(random.uniform(10, 28))
zb = H_TRUNK - random.uniform(0.3, 2.0)
c = trunk_center(zb)
rr = trunk_radius(zb, az) * 0.35
p0 = c + Vector((rr * math.cos(az), rr * math.sin(az), zb))
L = random.uniform(6.3, 8.0)
pts, dirs = grow_path(p0, dir_from(az, el), L, 14, 0.055, 0.11)
radii = lumpy(taper(random.uniform(0.50, 0.62), 0.10, len(pts), 0.75))
tube(bm_primary, pts, radii, 18)
spawn_children(pts, dirs, radii, 2, random.randint(3, 4), (0.40, 0.98))
tips.append((pts[-1], dirs[-1]))
# 가운데 위로 솟는 짧은 가지
for i in range(3):
az = random.uniform(0, 2 * math.pi)
p0 = trunk_center(H_TRUNK) + Vector((0.2 * math.cos(az), 0.2 * math.sin(az), H_TRUNK - 0.6))
pts, dirs = grow_path(p0, dir_from(az, math.radians(68)), random.uniform(3.0, 4.0), 9, 0.02, 0.06)
radii = taper(0.38, 0.08, len(pts), 0.8)
tube(bm_primary, pts, radii, 14)
spawn_children(pts, dirs, radii, 2, 2, (0.5, 0.95))
def build_stubs():
# 줄기 윗부분의 부러진 옛 가지 자국(짧은 뭉툭한 혹)
for i in range(5):
z = random.uniform(11.5, 16.5)
az = random.uniform(0, 2 * math.pi)
c = trunk_center(z)
r = trunk_radius(z, az)
d = dir_from(az, math.radians(random.uniform(20, 40)))
p0 = c + Vector((math.cos(az), math.sin(az), 0)) * (r * 0.75) + Vector((0, 0, z))
L = r * 0.25 + random.uniform(0.25, 0.45)
pts = [p0 + d * (L * k / 4) for k in range(5)]
rb = random.uniform(0.16, 0.24)
tube(bm_primary, pts, [rb * 1.3, rb * 1.15, rb, rb * 0.9, rb * 0.75], 12)
def finish(bm, name, material):
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
ob = bpy.data.objects.new(name, me)
link(ob)
me.materials.append(material)
return ob
# ---------------------------------------------------------------- 잎 뭉치 / 열매
def leaf_clump_mesh(name, seed):
rnd = random.Random(seed)
bm = bmesh.new()
# 손바닥 모양 겹잎: 잎자루 끝에서 퍼지는 5~7장의 납작한 타원 잎
for c in range(4):
cx, cy, cz = rnd.uniform(-0.25, 0.25), rnd.uniform(-0.25, 0.25), rnd.uniform(-0.1, 0.15)
nleaf = rnd.randint(5, 7)
rot0 = rnd.uniform(0, 2 * math.pi)
for k in range(nleaf):
a = rot0 + 2 * math.pi * k / nleaf
ln = rnd.uniform(0.22, 0.32)
w = ln * 0.32
# 잎 한 장: 4점 마름모꼴 + 가운데 접힘
dx, dy = math.cos(a), math.sin(a)
px, py = -dy, dx
droop = -0.06
v0 = bm.verts.new((cx, cy, cz))
v1 = bm.verts.new((cx + dx * ln * 0.5 + px * w, cy + dy * ln * 0.5 + py * w, cz + 0.02))
v2 = bm.verts.new((cx + dx * ln, cy + dy * ln, cz + droop))
v3 = bm.verts.new((cx + dx * ln * 0.5 - px * w, cy + dy * ln * 0.5 - py * w, cz + 0.02))
bm.faces.new((v0, v1, v2, v3))
me = bpy.data.meshes.new(name)
bm.to_mesh(me)
bm.free()
return me
def build_leaves():
meshes = [leaf_clump_mesh('LeafClump_A', 1), leaf_clump_mesh('LeafClump_B', 2), leaf_clump_mesh('LeafClump_C', 3)]
for m, mt in zip(meshes, (M_LEAF, M_LEAF2, M_LEAF)):
m.materials.append(mt)
for i, (p, d) in enumerate(tips):
for k in range(2):
ob = bpy.data.objects.new(f'LeafClump_{i:03d}_{k}', meshes[(i + k) % 3])
link(ob)
ob.location = p + Vector((random.uniform(-0.25, 0.25), random.uniform(-0.25, 0.25), random.uniform(-0.05, 0.25)))
ob.rotation_euler = (random.uniform(-0.3, 0.3), random.uniform(-0.3, 0.3), random.uniform(0, 6.28))
s = random.uniform(0.9, 1.3)
ob.scale = (s, s, s)
def build_fruits():
pod = lathe('Fruit_Pod_Mesh', [(0.0, 0.0), (0.035, 0.01), (0.06, 0.05), (0.07, 0.10), (0.065, 0.15),
(0.045, 0.19), (0.015, 0.21), (0.0, 0.215)], material=M_FRUIT, seg=14)
pod.name = 'Baobab_Fruit_000'
first = True
for i, (p, r) in enumerate(fruit_spots):
stalk_len = random.uniform(0.25, 0.45)
top = p + Vector((0, 0, -r * 0.6))
bot = top + Vector((0, 0, -stalk_len))
curve_tube(f'Fruit_Stalk_{i:03d}', [top + Vector((0, 0, 0.05)), bot], radius=0.008, material=M_TWIG,
resolution=1, bevel_res=1)
ob = pod if first else duplicate(pod, name=f'Baobab_Fruit_{i:03d}')
first = False
ob.location = bot + Vector((0, 0, -0.205))
ob.rotation_euler = (random.uniform(-0.1, 0.1), random.uniform(-0.1, 0.1), 0)
build_trunk()
build_crown()
build_stubs()
finish(bm_primary, 'Baobab_Branches_Primary', M_BARK_PALE)
finish(bm_secondary, 'Baobab_Branches_Secondary', M_BARK_PALE)
finish(bm_twig, 'Baobab_Twigs', M_TWIG)
build_leaves()
build_fruits()

다른 모형