
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
그랑디디에바오바브는 마다가스카르 서부에서만 자라는 바오바브로, 마다가스카르에 자생하는 바오바브 가운데 가장 크다. 이름은 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 randomfrom 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.55def 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 rdef 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 * patchdef 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 = Truedef 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, dirsdef 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 medef 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()






