
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
앵무조개(Nautilus pompilius)는 인도양과 서태평양의 산호초 바깥 비탈, 수심 수백 미터에 사는 두족류다. 오징어·문어와 같은 무리지만 바깥 껍데기를 그대로 지니고 있으며, 비슷한 모습의 조상이 수억 년 전부터 바다에 살아 '살아 있는 화석'으로 불린다.
껍데기 안쪽은 격벽으로 나뉜 여러 개의 방으로 이루어져 있고, 동물은 입구 쪽 마지막 방에만 산다. 앞쪽 방들은 가운데를 꿰뚫는 연실관으로 기체와 액체의 양을 조절해 부력을 맞춘다. 껍데기는 로그 나선을 따라 자라는데, 한 바퀴마다 반지름이 약 세 배로 커진다. 흔히 황금 나선과 같다고 소개되지만 실제 비율은 그와 다르다.
이 모델은 성체 껍데기 한 점을 실제 크기(지름 약 20cm)로 세워 놓은 모습이다. 바깥 소용돌이가 안쪽 소용돌이를 완전히 감싸 배꼽이 닫힌 형태, 배꼽에서 뻗어 나와 입구 쪽으로 갈수록 짧아지다 사라지는 적갈색 줄무늬, 입구 가장자리의 깔때기 홈과 얇은 두께, 입구 안쪽의 진주층과 검은 층, 그리고 안쪽 깊숙이 보이는 마지막 격벽과 연실관 구멍을 표현했다.
만든 방법
껍데기 전체는 하나의 bmesh 곡면으로, 달걀꼴 단면을 반지름이 한 바퀴에 3.1배 커지는 로그 나선을 따라 쓸어 만들었다. 입구 가장자리는 단면 각에 따라 나선 각을 앞뒤로 밀어 옆면은 앞으로 나오고 배 쪽은 오목하게 했고, 같은 모양을 따르는 약한 성장선을 바깥면에 넣었다. 줄무늬는 텍스처 대신 면마다 재질 번호를 수식(나선 각, 단면 위치, 고정 난수로 정한 줄마다의 길이·폭·흔들림)으로 골라 칠했기 때문에 가까이서 보면 줄 가장자리가 면 크기만큼 계단져 보인다. 안쪽 진주층 면과 마지막 격벽은 입구에서 들여다보이는 주거실 구간에만 만들었고, 그보다 안쪽의 방과 격벽은 보이지 않으므로 생략했다. 배꼽의 굳은살은 납작한 타원체로 단순화했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 203줄.
# 앵무조개(Nautilus pompilius) 껍데기 — 실제 크기(지름 약 20cm).# 껍데기는 로그 나선(한 바퀴에 반지름이 약 3.1배)을 따라 단면을 쓸어 만든 한 장의 bmesh 곡면이다.# 단면은 배 쪽이 둥글고 배꼽 쪽이 넓은 달걀꼴이며, 바깥 소용돌이가 안쪽 소용돌이를 완전히 감싸(포권) 배꼽이 닫힌다.# 호랑이 줄무늬는 텍스처 없이 면마다 재질 번호를 수식(나선 각·단면 위치·고정 난수)으로 골라 칠했다.# 입구 쪽에는 껍데기 두께, 진주층 안쪽 면, 마지막 격벽과 연실관 구멍, 검은 층(black layer)을 넣었다.import randomfrom atelier import *random.seed(7)SHOT = {"azimuth": -50, "elevation": 14, "lens": 70, "light": "studio", "background": "#1e2a33"}# ---------------------------------------------------------------- 재질M_SHELL = mat('Shell_Porcelain', '#ece0c6', rough=0.28, coat=0.5)M_STRIPE = mat('Stripe_RustBrown', '#a2552c', rough=0.3, coat=0.5)M_STRIPE_DK = mat('Stripe_DarkUmber', '#6f311a', rough=0.3, coat=0.5)M_NACRE = mat('Nacre_Pearl', '#ddd3cb', rough=0.16, metal=0.45)M_BLACK = mat('Black_Layer', '#2e2622', rough=0.35)M_CALLUS = mat('Umbilical_Callus', '#d9cdb8', rough=0.25, coat=0.4)M_SIPHO = mat('Siphuncle_Dark', '#4a3a2e', rough=0.6)MATS = [M_SHELL, M_STRIPE, M_STRIPE_DK, M_NACRE, M_BLACK, M_CALLUS, M_SIPHO]SHELL, STRIPE, STRIPE_DK, NACRE, BLACK, CALLUS, SIPHO = range(7)# ---------------------------------------------------------------- 나선 매개변수W = 3.1 # 소용돌이 확장률(한 바퀴당 반지름 배율)B = math.log(W) / (2 * math.pi)R0 = 0.128 # 입구 배 쪽까지의 반지름(m) → 지름 ≈ 0.2mTH = 0.0 # 입구의 나선 각(기준)C, A = 0.525, 0.475 # 단면 중심 / 반경 방향 반축 (R 배)LH = 0.36 # 옆 방향 반폭 (R 배)T = 0.0016 # 입구 껍데기 두께(m)NPHI = 144BODY = 2.1 # 주거실(마지막 격벽~입구) 길이(rad)def R(th): return R0 * math.exp(B * (th - TH))def section(th, phi, inset=0.0, g=0.0): """나선 각 th, 단면 각 phi(0=배 쪽, π=배꼽 쪽) 의 점. inset 만큼 안쪽으로 줄인 면, g 는 성장선 부풀림.""" r = R(th) a = (A * r - inset) * (1 + g) lh = (LH * r - inset) * (1 + g) cp, sp = math.cos(phi), math.sin(phi) rad = C * r + a * cp lat = lh * sp * (1.0 - 0.26 * cp) / 1.05 return Vector((rad * math.cos(th), lat, rad * math.sin(th)))나머지 155줄 펼치기접기
def sec_center(th): r = R(th) return Vector((C * r * math.cos(th), 0.0, C * r * math.sin(th)))def wrap(phi): return (phi + math.pi) % (2 * math.pi) - math.pidef margin_shift(th, phi): """입구 가장자리 굴곡: 옆면은 앞으로, 배 쪽은 깔때기 홈(hyponomic sinus)으로 오목.""" ramp = min(max((th - (TH - 0.35)) / 0.35, 0.0), 1.0) ramp = ramp * ramp * (3 - 2 * ramp) pw = wrap(phi) d = 0.09 * math.sin(phi) ** 2 * (1 - 0.3 * math.cos(phi)) - 0.13 * math.exp(-(pw / 0.42) ** 2) return th + ramp * ddef growth(th, phi): """성장선: 입구 가장자리 모양을 따라가는 잔물결(배꼽 쪽은 약하게).""" pw = wrap(phi) d = 0.09 * math.sin(phi) ** 2 - 0.13 * math.exp(-(pw / 0.42) ** 2) q = (th + d) * 26.0 w = 0.5 + 0.5 * math.cos(q + 0.6 * math.sin(q * 0.37)) return 0.0045 * w ** 6 * (0.4 + 0.6 * (1 + math.cos(phi)) / 2)# 나선 각 표본: 감춰진 안쪽 소용돌이는 성기게, 보이는 바깥 소용돌이는 촘촘히ths = []t = TH - 6 * math.piwhile t < TH - 2.4 * math.pi: ths.append(t) t += 1 / 22t = TH - 2.4 * math.pin_fine = int(2.4 * math.pi * 135)for i in range(n_fine + 1): ths.append(TH - 2.4 * math.pi + 2.4 * math.pi * i / n_fine)phis = [2 * math.pi * j / NPHI for j in range(NPHI)]# 줄무늬: 줄마다 고정 난수(길이·폭·흔들림)N_STRIPES_PER_TURN = 19stripe_rand = {}def srand(n): if n not in stripe_rand: stripe_rand[n] = (random.uniform(-0.25, 0.3), random.uniform(0.8, 1.15), random.uniform(0, 6.28)) return stripe_rand[n]def outer_color(th, phi): u = math.cos(phi) # +1 배 쪽, -1 배꼽 쪽 dlt = TH - th # 입구로부터의 각 거리 # 안쪽 소용돌이 가운데 주거실 안에 드러나는 배 쪽: 검은 층과 진주층 if dlt > 2 * math.pi - 0.12 and u > 0.15: return BLACK if dlt < 2 * math.pi + 0.45 else NACRE if u < -0.93: return CALLUS # 줄무늬 k = N_STRIPES_PER_TURN / (2 * math.pi) s = (u + 1) / 2 # 0 배꼽 → 1 배 p0 = th * k + 1.5 * s ** 1.3 n = math.floor(p0) ext, wid, ph = srand(n) p = p0 + 0.07 * math.sin(9 * s + ph) + 0.05 * math.sin(23 * s + 2 * ph) n = math.floor(p) ext, wid, ph = srand(n) width = (0.55 - 0.12 * s) * wid fade = min(max((dlt - 0.55) / 2.2, 0.0), 1.0) # 입구 쪽으로 갈수록 줄이 짧아진다 reach = -0.35 + 1.75 * fade + ext if dlt < 0.55 or s > reach: return SHELL # 끝부분은 가늘어지며 사라짐 taper = min(1.0, (reach - s) / 0.18) if (p - n) < width * (0.35 + 0.65 * taper): return STRIPE_DK if s < 0.28 else STRIPE return SHELL# ---------------------------------------------------------------- 껍데기 메시bm = bmesh.new()outer = []for th in ths: outer.append([bm.verts.new(section(margin_shift(th, ph), ph, g=growth(th, ph))) for ph in phis])faces_out = []for i in range(len(ths) - 1): thm = 0.5 * (ths[i] + ths[i + 1]) for j in range(NPHI): j2 = (j + 1) % NPHI f = bm.faces.new((outer[i][j], outer[i][j2], outer[i + 1][j2], outer[i + 1][j])) f.material_index = outer_color(thm, (j + 0.5) * 2 * math.pi / NPHI) faces_out.append(f)# 안쪽(진주층) 면: 마지막 격벽부터 입구까지i_sept = next(i for i, th in enumerate(ths) if th >= TH - BODY)inner = []for th in ths[i_sept:]: inner.append([bm.verts.new(section(margin_shift(th, ph), ph, inset=T)) for ph in phis])faces_in = []for i in range(len(inner) - 1): for j in range(NPHI): j2 = (j + 1) % NPHI f = bm.faces.new((inner[i][j], inner[i + 1][j], inner[i + 1][j2], inner[i][j2])) f.material_index = NACRE faces_in.append(f)# 입구 테두리(두께)for j in range(NPHI): j2 = (j + 1) % NPHI f = bm.faces.new((outer[-1][j], outer[-1][j2], inner[-1][j2], inner[-1][j])) f.material_index = SHELL# 마지막 격벽: 입구 쪽으로 오목한 접시, 가운데 연실관 구멍th_s = ths[i_sept]cen = sec_center(th_s)tan = Vector((-math.sin(th_s), 0, math.cos(th_s))) + B * Vector((math.cos(th_s), 0, math.sin(th_s)))tan.normalize()depth = 0.32 * A * R(th_s)K = 14prev = inner[0]for k in range(1, K + 1): s = 1.0 - k / K * 0.94 ring = [] for j, ph in enumerate(phis): p0 = inner[0][j].co ring.append(bm.verts.new(cen + (p0 - cen) * s - tan * depth * (1 - s * s))) for j in range(NPHI): j2 = (j + 1) % NPHI f = bm.faces.new((prev[j], prev[j2], ring[j2], ring[j])) f.material_index = SIPHO if s < 0.14 else NACRE prev = ringfc = bm.faces.new(list(reversed(prev)))fc.material_index = SIPHOme = bpy.data.meshes.new('Nautilus_Shell')bm.to_mesh(me)bm.free()shell = bpy.data.objects.new('Nautilus_Shell', me)link(shell)for m in MATS: me.materials.append(m)smooth(shell)# 배꼽을 덮는 굳은살(callus): 양옆 오목한 자리를 매끈하게 메운다callus = sphere('Umbilical_Callus', r=1.0, seg=32, rings=16, scale=(0.0165, 0.0124, 0.0165), material=M_CALLUS)# 살아 있을 때처럼 세운다: 입구는 아래, 입은 +X 쪽을 본다. 나선면은 XZ 평면(옆면이 -Y)# 회전은 메시에 직접 굽는다(바운딩 박스가 실제 형태에 딱 맞도록)POSE = Matrix.Rotation(math.radians(76), 4, 'Y')for ob in (shell, callus): ob.data.transform(POSE) ob.data.update()






