
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
파리지옥은 미국 노스캐롤라이나주와 사우스캐롤라이나주의 해안 평야 습지에만 자생하는 식충식물이다. 질소가 부족한 산성 토양에서 자라며, 부족한 양분을 잡은 곤충에서 얻는다. 잎은 땅에 붙은 로제트를 이루고, 각 잎은 넓적한 잎자루와 그 끝의 덫으로 나뉜다.
덫은 경첩처럼 이어진 두 장의 엽편으로 되어 있다. 안쪽 면에는 감각모가 몇 개씩 나 있어, 짧은 시간 안에 두 번 이상 건드려지면 엽편이 0.1초 남짓 만에 닫힌다. 가장자리의 긴 섬모는 닫힐 때 서로 엇갈려 창살처럼 먹이를 가두고, 이후 덫은 단단히 조여 며칠에 걸쳐 먹이를 소화한다.
이 모델은 물이끼 위에 자란 한 포기를 실제 크기로 재현했다. 열린 덫의 진홍색 안쪽과 연두색 테두리 띠, 바깥의 초록색, 가장자리 섬모와 감각모, 그리고 섬모가 깍지 낀 채 닫힌 덫을 함께 배치해 열림과 닫힘 두 상태를 한눈에 비교할 수 있게 했다.
만든 방법
덫은 중심맥 방향 u 와 단면 방향 v 를 두는 매개변수 곡면으로, 단면 각도를 적분해 오목한 두 엽편을 한 장의 메시로 접었다. 닫힌 덫은 테두리가 가운데에서 맞닿도록 열림 각을 이분 탐색으로 정했다. 두께는 Solidify 로 주면서 재질 오프셋을 써서 안쪽(진홍)·테두리 띠(연두)·바깥(초록)을 나눴고, 섬모는 테두리 곡선의 법선 방향으로 뻗은 가는 원뿔관으로 만들었다. 잎자루는 휘어진 중심선을 따라 폭이 넓어졌다 좁아지는 홈 진 띠이며, 잎 세 종류를 링크 복제해 크기와 방향을 바꿔 로제트로 배치했다. 바닥은 회전체 이탄 둔덕 위에 물이끼 머리를 고정 시드로 흩뿌린 것이며, 이끼의 잔가지 구조와 엽편 표면의 색 그러데이션, 꽃대는 생략했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 308줄.
# 파리지옥 (Dionaea muscipula) — 물이끼 위에 자란 한 포기# 덫은 매개변수 곡면으로 만들었다: 중심맥(경첩)을 따라 u, 단면을 따라 v 를 두고# 단면 각도를 적분해 오목한 조개 모양 두 잎(엽편)을 한 장의 곡면으로 접었다.# 안쪽은 진홍색, 가장자리 띠는 연두색, 바깥은 초록색이며 두께는 Solidify 로 주고 재질을 나눴다.# 가장자리 섬모(cilia)는 테두리 곡선의 법선을 따라 뻗은 가는 원뿔관이고, 닫힌 덫에서는 서로 엇갈린다.# 잎자루는 넓적한 날개형 띠를 곡선을 따라 휘었고, 잎 3종을 링크 복제해 로제트로 돌려 배치했다.# 바닥은 회전체 이탄 둔덕 위에 물이끼 머리(작은 구) 수백 개를 고정 시드로 흩뿌렸다.import randomfrom atelier import *random.seed(53)SHOT = {"azimuth": -60, "elevation": 36, "lens": 60, "light": "day", "background": "#2a333d"}M_LEAF = mat('LeafGreen', '#5b8a2c', rough=0.5)M_TRAP_IN = mat('TrapInnerCrimson', '#a51f2a', rough=0.32, coat=0.4)M_RIM = mat('TrapRimYellowGreen', '#a8b84a', rough=0.45)M_OUT = mat('TrapOuterGreen', '#679a32', rough=0.45)M_CILIA = mat('CiliaGreen', '#93a23e', rough=0.5)M_PEAT = mat('PeatSoil', '#3b2b1f', rough=0.95)M_MOSS = mat('SphagnumMoss', '#6f863a', rough=0.85)M_MOSS2 = mat('SphagnumMossPale', '#959a5a', rough=0.9)M_MOSS3 = mat('SphagnumMossDeep', '#566b2e', rough=0.85)def mesh_from(name, verts, faces, mats, face_mat=None): me = bpy.data.meshes.new(name) me.from_pydata(verts, [], faces) me.update() for m in mats: me.materials.append(m) if face_mat: for p, mi in zip(me.polygons, face_mat): p.material_index = mi for p in me.polygons: p.use_smooth = True ob = bpy.data.objects.new(name, me) link(ob) return obdef orient_faces(ob, center): """곡면 법선이 center(덫 안쪽)를 향하게 맞춘다.""" bm = bmesh.new() bm.from_mesh(ob.data) flip = [f for f in bm.faces if f.normal.dot(Vector(center) - f.calc_center_median()) < 0] bmesh.ops.reverse_faces(bm, faces=flip) bm.to_mesh(ob.data)나머지 260줄 펼치기접기
bm.free()def tube_mesh(verts, faces, pts, radii, sides=5): """pts 를 따라가는 끝이 뾰족한 관을 verts/faces 에 덧붙인다.""" rings = [] for i, p in enumerate(pts): if i < len(pts) - 1: t = (Vector(pts[i + 1]) - Vector(p)).normalized() else: t = (Vector(p) - Vector(pts[i - 1])).normalized() a = t.orthogonal().normalized() b = t.cross(a) r = radii[i] ring = [] for k in range(sides): ang = 2 * math.pi * k / sides ring.append(len(verts)) verts.append(tuple(Vector(p) + r * (math.cos(ang) * a + math.sin(ang) * b))) rings.append(ring) for i in range(len(rings) - 1): r0, r1 = rings[i], rings[i + 1] for k in range(sides): faces.append((r0[k], r0[(k + 1) % sides], r1[(k + 1) % sides], r1[k])) tip = len(verts) last = Vector(pts[-1]) + (Vector(pts[-1]) - Vector(pts[-2])).normalized() * radii[-1] * 3 verts.append(tuple(last)) for k in range(sides): faces.append((rings[-1][k], rings[-1][(k + 1) % sides], tip)) faces.append(tuple(reversed(rings[0])))# ---------------------------------------------------------------- 덫(trap)def section(alpha, cup, flare, n): """단면 곡선(경첩→테두리)을 각도 적분으로 만든다. 반환: [(y, z, angle)] (폭 1 기준).""" out = [(0.0, 0.0, alpha + cup)] y = z = 0.0 for i in range(1, n + 1): t = (i - 0.5) / n a = alpha + cup * (1 - t) + flare * t ** 5 y += math.sin(a) / n z += math.cos(a) / n out.append((y, z, alpha + flare)) return outdef make_trap(name, L, Wmax, closed=False): nu, nv = 36, 14 if closed: cup, flare = math.radians(55), math.radians(-8) lo, hi = math.radians(-30), math.radians(40) for _ in range(40): # 테두리가 가운데서 거의 맞닿도록 열림 각을 이분 탐색 mid = (lo + hi) / 2 if section(mid, cup, flare, nv)[-1][0] > 0.05: hi = mid else: lo = mid alpha = (lo + hi) / 2 else: alpha, cup, flare = math.radians(24), math.radians(38), math.radians(30) sec = section(alpha, cup, flare, nv) def width(u): return Wmax * max(math.sin(math.pi * u), 0.0) ** 0.6 * (0.85 + 0.3 * u) + Wmax * 0.03 verts, faces, fmat = [], [], [] grid = [] for i in range(nu + 1): u = i / nu x = L * u W = width(u) row = [] for j in range(-nv, nv + 1): sg = 1 if j >= 0 else -1 y, z, _a = sec[abs(j)] row.append(len(verts)) verts.append((x, sg * y * W, z * W)) grid.append(row) for i in range(nu): for j in range(2 * nv): faces.append((grid[i][j], grid[i + 1][j], grid[i + 1][j + 1], grid[i][j + 1])) t = abs((j + 0.5) - nv) / nv fmat.append(1 if t > 0.84 else 0) trap = mesh_from(name + 'Lobes', verts, faces, [M_TRAP_IN, M_RIM, M_OUT, M_OUT], fmat) orient_faces(trap, (L * 0.5, 0, Wmax * 0.9)) s = trap.modifiers.new('Solidify', 'SOLIDIFY') s.thickness = L * 0.03 s.offset = -1 s.material_offset = 2 s.material_offset_rim = 2 s.use_even_offset = True # 테두리 섬모와 감각모 cv, cf = [], [] n_cil = 17 clen = Wmax * (0.42 if not closed else 0.55) rad = L * 0.012 for sg in (1, -1): y1, z1, a_rim = sec[-1] for k in range(n_cil): u = 0.07 + 0.86 * (k + (0.5 if sg < 0 and closed else 0.0)) / (n_cil - (0 if closed else 1)) if u > 0.95: continue W = width(u) dW = (width(u + 1e-3) - width(u - 1e-3)) / 2e-3 / L base = Vector((L * u, sg * y1 * W, z1 * W)) if closed: ac = a_rim - math.radians(32) else: ac = a_rim + math.radians(18) e = Vector((0, sg * math.sin(ac), math.cos(ac))) d = (Vector((-dW, 0, 0)) * 0.9 + e).normalized() if closed: bend = Vector((0, -sg * 0.25, 0.15)) else: bend = Vector((0, sg * 0.25, -0.35)) length = clen * (0.75 + 0.25 * math.sin(math.pi * u)) p0 = base - d * rad * 2 p1 = base + d * length * 0.5 p2 = base + (d + bend * 0.5).normalized() * length tube_mesh(cv, cf, [tuple(p0), tuple(p1), tuple(p2)], [rad, rad * 0.75, rad * 0.4]) # 감각모: 각 엽편 안쪽에 3개 if not closed: for uu, tt in ((0.35, 0.45), (0.55, 0.5), (0.72, 0.42)): W = width(uu) j = int(tt * nv) y, z, _ = sec[j] pin = Vector((L * uu, sg * y * W, z * W)) nrm = Vector((0, -sg * math.cos(alpha), math.sin(alpha))).normalized() tube_mesh(cv, cf, [tuple(pin - nrm * 0.0004), tuple(pin + nrm * Wmax * 0.12), tuple(pin + nrm * Wmax * 0.22)], [rad * 0.5, rad * 0.35, rad * 0.2], 4) cil = mesh_from(name + 'Cilia', cv, cf, [M_CILIA]) return [trap, cil]# ---------------------------------------------------------------- 잎자루(petiole)def make_petiole(name, Lp, phi0, phi1, wmax): ns, nw = 34, 6 pts, angs = [], [] x = z = 0.0 for i in range(ns + 1): s = i / ns phi = phi0 + (phi1 - phi0) * s ** 0.7 pts.append((x, z)) angs.append(phi) x += math.cos(phi) * Lp / ns z += math.sin(phi) * Lp / ns def w(s): if s < 0.86: return 0.0018 + (wmax - 0.0018) * (s / 0.86) ** 1.4 k = (s - 0.86) / 0.14 return wmax * (1 - k) ** 1.6 + 0.0011 * k verts, faces = [], [] for i in range(ns + 1): s = i / ns cx, cz = pts[i] phi = angs[i] N = Vector((-math.sin(phi), 0, math.cos(phi))) ww = w(s) for j in range(nw + 1): q = -1 + 2 * j / nw off = 0.22 * ww * q * q # 가운데가 오목한 홈 p = Vector((cx, q * ww, cz)) + N * off verts.append(tuple(p)) for i in range(ns): for j in range(nw): a = i * (nw + 1) + j faces.append((a, a + 1, a + nw + 2, a + nw + 1)) ob = mesh_from(name + 'Petiole', verts, faces, [M_LEAF]) s = ob.modifiers.new('Solidify', 'SOLIDIFY') s.thickness = 0.0007 s.offset = 0 end = Vector((pts[-1][0], 0, pts[-1][1])) return ob, end, angs[-1]def make_leaf(name, Lp, phi0, phi1, wmax, L, Wmax, closed=False, trap_pitch=12): pet, end, phi = make_petiole(name, Lp, phi0, phi1, wmax) parts = make_trap(name, L, Wmax, closed) pitch = phi + math.radians(trap_pitch) for p in parts: p.rotation_euler = (0, -pitch, 0) p.location = end - Vector((math.cos(pitch), 0, math.sin(pitch))) * L * 0.02 leaf = join([pet] + parts, name) # 변환을 메시에 굽는다 leaf.data.transform(leaf.matrix_basis) leaf.matrix_basis = Matrix.Identity(4) return leafLEAF_OPEN = make_leaf('LeafOpenLarge', 0.062, math.radians(38), math.radians(8), 0.0074, 0.032, 0.0145)LEAF_MID = make_leaf('LeafOpenUpright', 0.048, math.radians(55), math.radians(22), 0.0066, 0.027, 0.0125, trap_pitch=18)LEAF_SHUT = make_leaf('LeafClosed', 0.055, math.radians(42), math.radians(14), 0.007, 0.029, 0.0125, closed=True)# ---------------------------------------------------------------- 바닥 둔덕R_MOUND, H_MOUND = 0.09, 0.015prof = [(0.0, H_MOUND)]for i in range(1, 15): r = R_MOUND * i / 14 prof.append((r, H_MOUND * (1 - (r / R_MOUND) ** 2.4) ** 0.6))prof.append((R_MOUND, 0.0))prof.append((0.0, 0.0))mound = lathe('PeatMound', prof, material=M_PEAT, seg=64)def mound_z(r): r = min(r, R_MOUND) return H_MOUND * (1 - (r / R_MOUND) ** 2.4) ** 0.6BASE_Z = H_MOUND - 0.003# ---------------------------------------------------------------- 로제트 배치layout = [ # (variant, azimuth deg, scale, roll deg) (LEAF_OPEN, 10, 1.00, 4), (LEAF_SHUT, 48, 0.95, -6), (LEAF_OPEN, 85, 0.92, -5), (LEAF_MID, 122, 1.0, 6), (LEAF_OPEN, 160, 1.05, 3), (LEAF_SHUT, 198, 0.9, 8), (LEAF_OPEN, 235, 0.97, -4), (LEAF_MID, 272, 0.95, 5), (LEAF_OPEN, 310, 1.02, -7), (LEAF_MID, 345, 0.85, 0), (LEAF_SHUT, 105, 0.72, -10), (LEAF_OPEN, 215, 0.74, 9), (LEAF_SHUT, 330, 0.7, 6), (LEAF_OPEN, 290, 0.7, -8), (LEAF_MID, 180, 0.72, 4), (LEAF_MID, 30, 0.55, 0), (LEAF_MID, 150, 0.5, 0), (LEAF_MID, 260, 0.52, 0),]for idx, (src, az, sc, roll) in enumerate(layout): ob = src if idx < 3 and src.get('placed') is None else duplicate(src) src['placed'] = True ob.name = f'Leaf_{idx:02d}_{src.name.split("_")[0]}' ob.location = (0, 0, BASE_Z) ob.rotation_euler = (math.radians(roll), 0, math.radians(az)) ob.scale = (sc, sc, sc)# 로제트 중심의 생장점crown = sphere('RosetteCrown', r=0.006, loc=(0, 0, BASE_Z), material=M_LEAF, seg=16, rings=8, scale=(1, 1, 0.6))# ---------------------------------------------------------------- 물이끼moss_a = sphere('SphagnumHead', r=0.0045, material=M_MOSS, seg=10, rings=6, scale=(1, 1, 0.65))moss_b = sphere('SphagnumHeadDry', r=0.004, material=M_MOSS2, seg=10, rings=6, scale=(1, 1, 0.6))moss_c = sphere('SphagnumHeadDeep', r=0.005, material=M_MOSS3, seg=10, rings=6, scale=(1, 1, 0.7))moss_srcs = [moss_a, moss_b, moss_c]for m_, (x_, y_) in zip(moss_srcs, ((0.02, 0.0), (-0.03, 0.01), (0.0, -0.035))): m_.location = (x_, y_, mound_z(math.hypot(x_, y_)) - 0.0008)count = 0tries = 0placed = [(0.02, 0.0), (-0.03, 0.01), (0.0, -0.035)]while count < 680 and tries < 20000: tries += 1 r = R_MOUND * 0.97 * math.sqrt(random.random()) a = random.random() * 2 * math.pi x, y = r * math.cos(a), r * math.sin(a) if any((x - px) ** 2 + (y - py) ** 2 < 0.0042 ** 2 for px, py in placed): continue placed.append((x, y)) src = random.choices(moss_srcs, weights=(5, 2, 3))[0] s = random.uniform(0.75, 1.3) duplicate(src, loc=(x, y, mound_z(r) - 0.0008 - random.uniform(0, 0.0015)), rot=(random.uniform(-0.3, 0.3), random.uniform(-0.3, 0.3), random.random() * 6.28), scale=(s, s, s * random.uniform(0.8, 1.3)), name='SphagnumHead') count += 1






