
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
해바라기는 북아메리카가 원산인 국화과의 한해살이풀로, 오래전부터 씨앗과 기름을 얻기 위해 재배되어 왔다. 흔히 한 송이 꽃으로 보는 머리 부분은 사실 수많은 작은 꽃이 모인 두상화서다. 가장자리의 노란 '꽃잎'은 각각 하나의 혀꽃이고, 가운데 갈색 원반은 씨앗이 될 관상화 수백~수천 개로 채워져 있다.
원반의 관상화는 이웃한 꽃과 약 137.5°(황금각)씩 어긋나며 안쪽에서 바깥으로 차례로 생겨난다. 이 규칙 때문에 원반에는 시계 방향과 반시계 방향의 나선이 겹쳐 보이며, 그 개수는 흔히 34와 55, 55와 89처럼 피보나치 수열의 이웃한 두 수가 된다. 어린 해바라기는 해를 따라 고개를 돌리지만, 꽃이 다 자라면 대개 동쪽을 향한 채 무거운 머리를 숙인다.
이 모델은 다 자란 해바라기 한 그루를 높이 약 1.9m, 원반 지름 30cm 의 실제 크기로 재현했다. 굽은 목 끝에서 앞으로 숙인 꽃머리, 두 겹의 혀꽃잎, 뒷면을 비늘처럼 덮은 녹색 총포, 줄기를 따라 어긋나게 달린 톱니 가장자리의 심장형 잎을 표현했다.
만든 방법
원반의 관상화 2,000개는 r = c·√n, θ = n·137.5° 공식으로 하나씩 육각 원뿔대를 놓아, 나선 무늬가 따로 그리지 않아도 배치 자체에서 드러나게 했다. 혀꽃잎·총포 조각·잎은 하나의 bmesh 격자 함수로 만들었으며 폭 윤곽, 가장자리 들림, 길이 방향 처짐, 가운데맥 접힘, 비틀림 값만 바꿔 쓴다. 줄기와 잎자루는 경로를 따라 굵기가 줄어드는 관을 평행 이동 프레임으로 감아 만들었고, 꽃받침 그릇은 회전체다. 잎맥은 가운데맥과 곁맥 세 쌍만 얇은 관으로 돋웠고, 줄기의 잔털과 씨앗 하나하나의 줄무늬는 생략했다. 무작위 변화는 고정된 시드로 만들어 실행할 때마다 같은 모양이 나온다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 355줄.
# 해바라기 (Helianthus annuus) — 한 그루를 실제 크기(높이 약 1.9m)로 코드만으로 만든 모델.# 꽃 가운데 원반의 관상화(작은 꽃) 2,000개는 황금각(137.5°) 엽서(phyllotaxis) 공식# r = c·√n, θ = n·137.5° 로 하나씩 놓아, 해바라기 특유의 나선(시계·반시계 방향 34/55개)이 저절로 드러난다.# 혀꽃잎·총포 조각·잎은 모두 같은 '잎사귀 격자' 함수(길이·폭 윤곽, 오목함, 처짐, 비틀림)로 bmesh 를 빚었고,# 줄기와 잎자루는 경로를 따라 굵기가 줄어드는 관(평행 이동 프레임)으로, 꽃 뒷면 꽃받침은 회전체로 만들었다.# 무작위 값은 random.seed 로 고정해 늘 같은 모양이 나온다.import randomfrom atelier import *random.seed(1883)SHOT = {"azimuth": -72, "elevation": 10, "lens": 60, "light": "golden", "background": "#24303a"}GOLDEN = math.radians(137.50776)# ---------------------------------------------------------------- 재질M_PETAL = mat("Ray_Petal_Yellow", "#f0b40c", rough=0.55)M_PETAL2 = mat("Ray_Petal_Deep", "#e39a0a", rough=0.55)M_DISC_BASE = mat("Disc_Receptacle_Dark", "#2a180c", rough=0.9)M_FLORET = mat("Disc_Floret_Brown", "#4e2c13", rough=0.85)M_FLORET_IN = mat("Disc_Floret_Center", "#5f5a22", rough=0.8)M_FLORET_OUT = mat("Disc_Floret_Pollen", "#b8781c", rough=0.75)M_STEM = mat("Stem_Green", "#5d8030", rough=0.65)M_LEAF = mat("Leaf_Green", "#3e6a24", rough=0.6)M_VEIN = mat("Leaf_Vein", "#7d9c48", rough=0.6)M_BRACT = mat("Bract_Green", "#4b7428", rough=0.6)M_SOIL = mat("Garden_Soil", "#3e2c21", rough=0.95)# ---------------------------------------------------------------- 도우미def obj_from_bm(name, bm, material, smooth_shade=True): me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) link(ob) assign(ob, material) if smooth_shade: smooth(ob) return obdef bezier(p0, p1, p2, p3, n): out = [] for i in range(n + 1): t = i / n u = 1 - t out.append(p0 * u ** 3 + p1 * 3 * u * u * t + p2 * 3 * u * t * t + p3 * t ** 3)나머지 307줄 펼치기접기
return outdef tube_into(bm, pts, radii, seg=12, rib=0.0, cap_start=True, cap_end=True): """경로(pts)를 따라 반지름(radii)이 변하는 관. 평행 이동 프레임으로 비틀림 없이 감는다.""" 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((1, 0, 0)) if abs(tans[0].x) < 0.9 else Vector((0, 1, 0)) nrm = tans[0].cross(ref).normalized() rings = [] for i in range(n): if i > 0: rot = tans[i - 1].rotation_difference(tans[i]) nrm = (rot @ nrm).normalized() bin_ = tans[i].cross(nrm).normalized() ring = [] for k in range(seg): a = 2 * math.pi * k / seg r = radii[i] * (1 + rib * math.cos(a * 8)) ring.append(bm.verts.new(pts[i] + (nrm * math.cos(a) + bin_ * math.sin(a)) * r)) rings.append(ring) for i in range(n - 1): for k in range(seg): bm.faces.new((rings[i][k], rings[i][(k + 1) % seg], rings[i + 1][(k + 1) % seg], rings[i + 1][k])) if cap_start: bm.faces.new(list(reversed(rings[0]))) if cap_end: bm.faces.new(rings[-1]) return ringsdef blade_into(bm, matrix, L, W, width_fn, nu=12, nv=6, cup=0.0, droop=0.0, lift=0.0, twist=0.0, fold=0.0, notch=0.0, t0=0.0, serr=0.0, serr_n=0): """잎·꽃잎 한 장. 로컬 +X 가 길이, Y 가 폭, +Z 가 앞면. 좌우 반쪽을 따로 만든 뒤 가운데맥에서 붙인다. width_fn(t)→0..1 폭 윤곽, cup: 가장자리 들림, droop/lift: 길이 방향 휨, twist: 끝 비틀림(rad), fold: 가운데맥 V 접힘, notch/t0: 심장형 잎 밑동의 오목한 홈.""" grid = [] for side in (-1, 1): rows = [] for i in range(nu + 1): t = t0 + (1 - t0) * i / nu w = W * width_fn(t) if serr and serr_n: ph = (t * serr_n) % 1.0 w *= 1 + serr * (ph - 0.5) s_in = 0.0 if t < 0: s_in = notch * W * (t / t0) row = [] for j in range(nv + 1): s = j / nv y = side * (s_in + s * max(w - s_in, 0.0)) x = t * L yn = y / max(W, 1e-6) z = cup * W * yn * yn - droop * L * t * t + lift * L * t - fold * abs(y) ang = twist * max(t, 0) yy = y * math.cos(ang) - z * math.sin(ang) zz = y * math.sin(ang) + z * math.cos(ang) row.append(bm.verts.new(matrix @ Vector((x, yy, zz)))) rows.append(row) grid.append((side, rows)) for side, rows in grid: for i in range(nu): for j in range(nv): q = (rows[i][j], rows[i + 1][j], rows[i + 1][j + 1], rows[i][j + 1]) if side < 0: q = tuple(reversed(q)) bm.faces.new(q)# ---------------------------------------------------------------- 꽃머리 (로컬: 원반 앞면이 +Z)RD = 0.150 # 원반 반지름 (지름 30cm)DOME = 0.014 # 원반의 볼록한 정도def dome_z(r): return DOME * (1 - (r / RD) ** 2)head_parts = []# 원반 바탕(관상화 아래의 짙은 면)prof = [(0.0, dome_z(0) - 0.002)]for i in range(1, 13): r = RD * 1.03 * i / 12 prof.append((r, dome_z(min(r, RD)) - 0.002 - (0.004 if r > RD else 0)))disc = lathe("Disc_Base", prof, material=M_DISC_BASE, seg=96)head_parts.append(disc)# 관상화 — 황금각 나선 배치N_FLORETS = 2000c = RD / math.sqrt(N_FLORETS)bms = {"in": bmesh.new(), "mid": bmesh.new(), "out": bmesh.new()}for n in range(1, N_FLORETS + 1): r = c * math.sqrt(n) th = n * GOLDEN x, y = r * math.cos(th), r * math.sin(th) z = dome_z(r) - 0.002 nrm = Vector((2 * DOME * x / RD ** 2, 2 * DOME * y / RD ** 2, 1)).normalized() rho = r / RD if rho < 0.22: key, h, s = "in", c * 1.0, c * 0.80 elif rho < 0.86: key, h, s = "mid", c * (1.3 + 0.5 * rho), c * 0.84 else: key, h, s = "out", c * 2.2, c * 0.70 m = (Matrix.Translation(Vector((x, y, z)) + nrm * (h / 2)) @ Vector((0, 0, 1)).rotation_difference(nrm).to_matrix().to_4x4() @ Matrix.Rotation(th, 4, 'Z')) bmesh.ops.create_cone(bms[key], cap_ends=True, segments=6, radius1=s, radius2=s * 0.6, depth=h, matrix=m)head_parts.append(obj_from_bm("Disc_Florets_Center", bms["in"], M_FLORET_IN, False))head_parts.append(obj_from_bm("Disc_Florets_Spiral", bms["mid"], M_FLORET, False))head_parts.append(obj_from_bm("Disc_Florets_Open_Ring", bms["out"], M_FLORET_OUT, False))# 꽃 뒷면 꽃받침(녹색 그릇)back_prof = [(0.014, -0.095), (0.035, -0.091), (0.065, -0.082), (0.095, -0.068), (0.12, -0.050), (0.139, -0.032), (0.151, -0.018), (RD * 1.035, -0.008), (RD * 1.03, dome_z(RD) - 0.004)]receptacle = lathe("Receptacle_Back", back_prof, material=M_BRACT, seg=96)head_parts.append(receptacle)def petal_w(t): return 0.32 * (1 - t) + 0.75 * math.sin(math.pi * t ** 0.72) ** 0.9 if t < 1 else 0.0# 혀꽃(노란 꽃잎) 두 겹bm_p1, bm_p2 = bmesh.new(), bmesh.new()for row, (count, r0, z0, tilt, Lb, bm_t) in enumerate([(32, RD * 0.96, -0.006, 2, 0.125, bm_p1), (32, RD * 0.92, -0.002, 12, 0.110, bm_p2)]): for i in range(count): th = 2 * math.pi * (i + 0.5 * row) / count + random.uniform(-0.04, 0.04) L = Lb * random.uniform(0.88, 1.1) W = 0.023 * random.uniform(0.9, 1.1) tl = math.radians(tilt + random.uniform(-6, 6)) m = (Matrix.Rotation(th, 4, 'Z') @ Matrix.Translation((r0, 0, z0)) @ Matrix.Rotation(-tl, 4, 'Y')) blade_into(bm_t, m, L, W, petal_w, nu=10, nv=3, cup=0.35, droop=random.uniform(0.05, 0.22), twist=random.uniform(-0.35, 0.35), fold=0.15)p_out = obj_from_bm("Ray_Florets_Outer", bm_p1, M_PETAL)p_in = obj_from_bm("Ray_Florets_Inner", bm_p2, M_PETAL2)for p in (p_out, p_in): solidify(p, 0.0012)head_parts += [p_out, p_in]def bract_w(t): return 0.85 * math.sin(math.pi * min(t, 1) ** 0.8) ** 0.8 + 0.25 * (1 - t)# 총포 조각(꽃 뒷면을 비늘처럼 덮은 뾰족한 녹색 잎조각) — 그릇 면을 따라 눕히고 끝은 바깥으로 젖힌다def bowl_at(r): for (r0, z0), (r1, z1) in zip(back_prof, back_prof[1:]): if r0 <= r <= r1: a = (r - r0) / (r1 - r0) return z0 + (z1 - z0) * a, Vector((r1 - r0, 0, z1 - z0)).normalized() return back_prof[-1][1], Vector((0, 0, 1))bm_b = bmesh.new()N_BRACT = 110for k in range(N_BRACT): th = k * GOLDEN f = k / (N_BRACT - 1) rr = 0.055 + 0.088 * f ** 0.7 zz, tang = bowl_at(rr) out_n = Vector((tang.z, 0, -tang.x)) # 그릇 바깥쪽 법선 pos = Vector((rr, 0, zz)) + out_n * 0.0015 basis = Matrix((tang, out_n.cross(tang), out_n)).transposed().to_4x4() m = Matrix.Rotation(th, 4, 'Z') @ Matrix.Translation(pos) @ basis L = (0.040 + 0.040 * f) * random.uniform(0.85, 1.15) blade_into(bm_b, m, L, 0.012 + 0.006 * f, bract_w, nu=7, nv=2, cup=-0.15, droop=-(0.10 + 0.45 * f), twist=random.uniform(-0.2, 0.2), fold=0.15)bracts = obj_from_bm("Involucral_Bracts", bm_b, M_BRACT)solidify(bracts, 0.0015)head_parts.append(bracts)# 꽃머리 자세: 앞(-Y)을 보며 조금 위로, 살짝 옆으로 돌아 고개를 숙인 듯HEAD_C = Vector((0.03, -0.30, 1.62))HEAD_ROT = Euler((math.radians(72), 0, math.radians(-8)))head = bpy.data.objects.new("Sunflower_Head", None)link(head)head.location = HEAD_Chead.rotation_euler = HEAD_ROTfor p in head_parts: p.parent = headN_HEAD = HEAD_ROT.to_matrix() @ Vector((0, 0, 1))# ---------------------------------------------------------------- 줄기STEM_TOP = 1.34stem_pts, stem_r = [], []for i in range(71): z = STEM_TOP * i / 70 sway = 0.03 * math.sin(z * 1.9) * (z / STEM_TOP) stem_pts.append(Vector((sway, 0.012 * math.sin(z * 2.7), z))) stem_r.append(0.024 - 0.010 * (z / STEM_TOP))back = HEAD_C - N_HEAD * 0.080p0 = stem_pts[-1]neck = bezier(p0, p0 + Vector((0, 0.01, 0.16)), back - N_HEAD * 0.17, back, 26)[1:]for i, p in enumerate(neck): t = (i + 1) / len(neck) stem_pts.append(p) stem_r.append(0.013 + 0.004 * t ** 3)stem_pts[0] = stem_pts[0] - Vector((0, 0, 0.03)) # 흙 속까지 박힘bm = bmesh.new()tube_into(bm, stem_pts, stem_r, seg=16, rib=0.05)stem = obj_from_bm("Stem", bm, M_STEM)def stem_at(z): """수직 구간의 줄기 위치와 반지름.""" f = min(max(z / STEM_TOP, 0), 1) * 70 i = min(int(f), 69) a = f - i return stem_pts[i].lerp(stem_pts[i + 1], a), stem_r[i] + (stem_r[i + 1] - stem_r[i]) * a# ---------------------------------------------------------------- 잎 (심장형, 어긋나기)def leaf_w(t): if t < 0: return 0.55 * math.sqrt(max(0.0, 1 - (t / -0.16) ** 2)) return (1 - t) ** 0.9 * (0.55 + 1.6 * t) / (1 + 1.6 * t * t)bm_leaf, bm_pet, bm_vein = bmesh.new(), bmesh.new(), bmesh.new()N_LEAVES = 14for i in range(N_LEAVES): f = i / (N_LEAVES - 1) z = 0.16 + f * 1.08 phi = math.radians(30) + i * GOLDEN base, sr = stem_at(z) d = Vector((math.cos(phi), math.sin(phi), 0)) up = Vector((0, 0, 1)) pl = 0.16 - 0.06 * f # 잎자루 길이 L = (0.42 - 0.22 * f) * random.uniform(0.92, 1.05) # 잎몸 길이 W = L * 0.72 q0 = base + d * sr * 0.4 q3 = q0 + d * pl * 0.92 + up * pl * (0.55 - 0.2 * f) pet = bezier(q0, q0 + up * pl * 0.35 + d * pl * 0.1, q3 - d * pl * 0.3, q3, 10) tube_into(bm_pet, pet, [0.0055 - 0.0025 * k / 10 - 0.001 * f for k in range(11)], seg=8) pitch = math.radians(42 - 12 * f) m = (Matrix.Translation(q3) @ Matrix.Rotation(phi, 4, 'Z') @ Matrix.Rotation(-pitch, 4, 'Y') @ Matrix.Rotation(random.choice((-1, 1)) * random.uniform(0.35, 0.7), 4, "X")) droop = 0.95 - 0.35 * f blade_into(bm_leaf, m, L, W, leaf_w, nu=34, nv=6, t0=-0.16, notch=0.22, droop=droop, cup=-0.06, fold=0.30, serr=0.07, serr_n=16) def leaf_pt(t, y, lift_=0.0018): z = -0.06 * W * (y / W) ** 2 - droop * L * t * t - 0.30 * abs(y) + lift_ return m @ Vector((t * L, y, z)) # 가운데맥과 곁맥(잎 앞면 위로 살짝 돋음) mid = [leaf_pt(k / 12 * 0.92, 0.0) for k in range(13)] tube_into(bm_vein, mid, [0.0022 * (1 - k / 13) + 0.0006 for k in range(13)], seg=6) for tv in (0.04, 0.22, 0.42): for sd in (-1, 1): reach = W * leaf_w(min(tv + 0.25, 1.0)) * 0.75 vp = [leaf_pt(tv + 0.25 * u, sd * reach * u ** 0.9) for u in [k / 6 for k in range(7)]] tube_into(bm_vein, vp, [0.0011 * (1 - k / 8) + 0.0003 for k in range(7)], seg=5)leaves = obj_from_bm("Leaves", bm_leaf, M_LEAF)solidify(leaves, 0.0012)obj_from_bm("Petioles", bm_pet, M_STEM)obj_from_bm("Leaf_Midribs", bm_vein, M_VEIN)# ---------------------------------------------------------------- 흙 둔덕def soil_h(r, th, R): q = min(r / R, 1.0) return 0.075 * (1 - q * q) ** 1.6 * (1 + 0.12 * math.sin(5 * th + 2 * q) + 0.06 * math.sin(11 * th + 1))bm = bmesh.new()NA, NR = 64, 10rim = [0.27 * (1 + 0.07 * math.sin(3 * (2 * math.pi * k / NA) + 1) + 0.04 * math.sin(7 * (2 * math.pi * k / NA))) for k in range(NA)]top = bm.verts.new((0, 0, soil_h(0, 0, 1)))rings = []for j in range(1, NR + 1): ring = [] for k in range(NA): th = 2 * math.pi * k / NA r = rim[k] * j / NR ring.append(bm.verts.new((r * math.cos(th), r * math.sin(th), soil_h(r, th, rim[k])))) rings.append(ring)for k in range(NA): bm.faces.new((top, rings[0][k], rings[0][(k + 1) % NA]))for j in range(NR - 1): for k in range(NA): bm.faces.new((rings[j][k], rings[j + 1][k], rings[j + 1][(k + 1) % NA], rings[j][(k + 1) % NA]))bm.faces.new(list(reversed(rings[-1])))bmesh.ops.recalc_face_normals(bm, faces=bm.faces)obj_from_bm("Soil_Mound", bm, M_SOIL)# 흙덩이 몇 개(반쯤 묻힘)bm = bmesh.new()for k in range(16): th = random.uniform(0, 2 * math.pi) r = random.uniform(0.06, 0.24) s = random.uniform(0.010, 0.022) R = 0.27 z = soil_h(r, th, R) mtx = (Matrix.Translation((r * math.cos(th), r * math.sin(th), z)) @ Matrix.Rotation(random.uniform(0, 3), 4, 'Z') @ Matrix.Diagonal((s * random.uniform(1, 1.6), s, s * 0.7, 1))) bmesh.ops.create_icosphere(bm, subdivisions=1, radius=1.0, matrix=mtx)obj_from_bm("Soil_Clods", bm, M_SOIL, smooth_shade=False)






