본문으로 건너뛰기
3D 아틀리에
해바라기 3D 모형

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

이 모형에 대하여

해바라기는 북아메리카가 원산인 국화과의 한해살이풀로, 오래전부터 씨앗과 기름을 얻기 위해 재배되어 왔다. 흔히 한 송이 꽃으로 보는 머리 부분은 사실 수많은 작은 꽃이 모인 두상화서다. 가장자리의 노란 '꽃잎'은 각각 하나의 혀꽃이고, 가운데 갈색 원반은 씨앗이 될 관상화 수백~수천 개로 채워져 있다.

원반의 관상화는 이웃한 꽃과 약 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 random
from 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 ob
def 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 out
def 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 rings
def 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 = 2000
c = 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 = 110
for 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_C
head.rotation_euler = HEAD_ROT
for p in head_parts:
p.parent = head
N_HEAD = HEAD_ROT.to_matrix() @ Vector((0, 0, 1))
# ---------------------------------------------------------------- 줄기
STEM_TOP = 1.34
stem_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.080
p0 = 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 = 14
for 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, 10
rim = [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)

다른 모형