
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
모카포트는 아래 보일러의 물이 끓으며 생긴 증기 압력으로 뜨거운 물을 분쇄 커피층에 밀어 올려, 위쪽 통에 진한 커피를 모으는 스토브탑 커피 메이커다. 1933년 이탈리아의 알폰소 비알레티가 내놓은 '모카 익스프레스'가 원형이며, 디자인은 거의 바뀌지 않은 채 오늘날까지 생산되고 있다.
알루미늄 주조로 만든 팔각형 몸통은 이 제품의 가장 큰 특징이다. 아래 보일러와 위 커피통이 나사로 맞물리는 자리에서 몸통이 잘록해져 모래시계 같은 실루엣이 생기고, 각진 면마다 빛이 다르게 맺힌다. 검은 손잡이와 뚜껑 꼭지, 앞쪽의 뾰족한 주둥이, 보일러 옆면의 안전밸브가 함께 알아보기 쉬운 형태를 이룬다.
이 모델은 6컵 크기(높이 약 22cm)를 기준으로, 뚜껑을 닫은 상태의 외형을 재현했다. 보일러와 커피통의 기울기, 이음 칼라, 말린 테두리, 경첩과 엄지 꼬리, 커피통 옆면의 타원 배지를 표현했으며, 배지 안의 상표 그림은 넣지 않았다.
만든 방법
보일러·이음 칼라·커피통·뚜껑은 (반폭, 높이) 측면 프로필을 8각으로 돌린 회전체로 만들어 면마다 각이 서게 했고, 각도 기준 베벨 모디파이어로 모서리에 가는 하이라이트를 넣었다. 손잡이는 닫힌 중심선을 캣멀-롬 곡선으로 보간한 뒤 폭과 두께가 변하는 사각 단면 고리를 bmesh로 짜고 서브서프로 둥글렸다. 주둥이는 벽에서 나와 위로 휘어 나가는 용골선과 양옆 모서리를 잇는 삼각형 부채꼴 면으로 만들었다. 안전밸브와 배지는 기울어진 면의 경사를 계산해 면에 수직으로 붙였다. 내부의 깔때기 필터, 고무 가스켓, 커피 관, 나사산은 닫힌 상태에서 보이지 않으므로 생략했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 235줄.
# 모카포트 (비알레띠 모카 익스프레스형, 6컵) — 실제 크기(높이 약 22cm)로 코드만으로 만든 모델.# 아래 보일러와 위 커피통은 '팔각 회전체'다: (반폭, 높이) 측면 프로필을 8각으로 돌려 면이 각진 알루미늄 몸통을 만들고,# 베벨 모디파이어로 모서리마다 가는 하이라이트가 서도록 했다. 두 몸통이 만나 잘록한 허리(나사 이음 칼라)가 생긴다.# 손잡이는 닫힌 중심선을 따라 두께·폭이 변하는 사각 단면 고리를 bmesh 로 짠 뒤 서브서프로 둥글린 검은 수지 부품,# 뚜껑은 팔각 낮은 피라미드에 검은 꼭지, 앞쪽에는 뾰족한 주둥이, 보일러 옆면에는 육각 안전밸브를 달았다.# 내부(깔때기 필터, 가스켓, 커피 관)는 뚜껑을 닫은 상태라 보이지 않으므로 만들지 않았다.from atelier import *SHOT = {"azimuth": -58, "elevation": 18, "lens": 70, "light": "studio", "background": "#2b2724"}ALU = mat('Aluminium_Body', '#aeb1b3', rough=0.30, metal=1.0)ALU_CAST = mat('Aluminium_Cast', '#9c9e9e', rough=0.46, metal=1.0)STEEL = mat('Steel_Valve', '#cfccc6', rough=0.22, metal=1.0)RESIN = mat('Black_Resin', '#1f1d1c', rough=0.42)BADGE = mat('Badge_Enamel', '#5c1f1b', rough=0.40)COS8 = math.cos(math.pi / 8) # 팔각형: 꼭짓점 반경 = 반폭 / cos(22.5°)FACET0 = math.pi / 8 # 면이 ±X, ±Y 를 향하도록 꼭짓점을 22.5° 돌린다def oct_lathe(name, profile, material, cap_bottom=False, cap_top=False, bevel_w=0.0008): """profile=[(반폭 a, z), ...] 를 팔각으로 돌린 각진 회전체.""" bm = bmesh.new() rings = [] for a, z in profile: R = a / COS8 rings.append([bm.verts.new((R * math.cos(FACET0 + k * math.pi / 4), R * math.sin(FACET0 + k * math.pi / 4), z)) for k in range(8)]) for j in range(len(rings) - 1): for k in range(8): bm.faces.new((rings[j][k], rings[j][(k + 1) % 8], rings[j + 1][(k + 1) % 8], rings[j + 1][k])) if cap_bottom: bm.faces.new(list(reversed(rings[0]))) if cap_top: bm.faces.new(rings[-1]) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) 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 bevel_w: m = ob.modifiers.new('EdgeBevel', 'BEVEL') m.width = bevel_w m.segments = 3 m.limit_method = 'ANGLE' m.angle_limit = math.radians(20)나머지 187줄 펼치기접기
m.harden_normals = True return obdef apothem_at(profile, z): """프로필에서 높이 z 의 반폭(선형 보간).""" for (a0, z0), (a1, z1) in zip(profile, profile[1:]): if z0 <= z <= z1 and z1 > z0: t = (z - z0) / (z1 - z0) return a0 + (a1 - a0) * t return profile[-1][0]# ---------------------------------------------------------------- 보일러(아래 물통)BOILER = [ (0.0515, 0.0000), (0.0540, 0.0020), (0.0545, 0.0060), # 바닥 굽 (0.0532, 0.0085), (0.0512, 0.0200), (0.0478, 0.0380), (0.0436, 0.0560), (0.0398, 0.0700), (0.0378, 0.0770), (0.0372, 0.0800),]oct_lathe('Boiler', BOILER, ALU, cap_bottom=True)# ---------------------------------------------------------------- 커피통(위 몸통) + 나사 이음 칼라COLLAR = [ (0.0380, 0.0800), (0.0398, 0.0810), (0.0408, 0.0880), (0.0412, 0.0895), (0.0396, 0.0910),]oct_lathe('Collector_Collar', COLLAR, ALU_CAST, cap_bottom=True)COLLECTOR = [ (0.0380, 0.0910), (0.0382, 0.0950), (0.0408, 0.1200), (0.0443, 0.1450), (0.0480, 0.1680), (0.0496, 0.1750), (0.0512, 0.1760), (0.0516, 0.1785), (0.0490, 0.1790), # 말린 테두리]oct_lathe('Collector', COLLECTOR, ALU)# ---------------------------------------------------------------- 뚜껑 + 꼭지LID_Z = 0.1790LID = [ (0.0508, LID_Z), (0.0512, LID_Z + 0.0015), (0.0508, LID_Z + 0.0035), (0.0420, LID_Z + 0.0100), (0.0300, LID_Z + 0.0165), (0.0170, LID_Z + 0.0205), (0.0120, LID_Z + 0.0212),]oct_lathe('Lid', LID, ALU, cap_top=True)LID_TOP = LID_Z + 0.0212knob = lathe('Lid_Knob', [ (0.0001, LID_TOP - 0.0005), (0.0095, LID_TOP - 0.0005), (0.0090, LID_TOP + 0.0015), (0.0062, LID_TOP + 0.0045), (0.0068, LID_TOP + 0.0070), (0.0105, LID_TOP + 0.0110), (0.0112, LID_TOP + 0.0140), (0.0098, LID_TOP + 0.0170), (0.0060, LID_TOP + 0.0190), (0.0001, LID_TOP + 0.0196),], material=RESIN, seg=64)# ---------------------------------------------------------------- 주둥이(+X 면)def spout(): """윗 테두리에서 앞으로 뻗은 뾰족한 주둥이. 용골선이 벽에서 나와 위로 휘어 나간다.""" a_top = 0.0512 z_top = 0.1785 z_low = z_top - 0.050 bm = bmesh.new() tip = bm.verts.new((a_top + 0.0235, 0, z_top + 0.0045)) l = bm.verts.new((a_top - 0.004, -0.0215, z_top)) r = bm.verts.new((a_top - 0.004, 0.0215, z_top)) l2 = bm.verts.new((a_top - 0.004, -0.0205, z_top - 0.0028)) r2 = bm.verts.new((a_top - 0.004, 0.0205, z_top - 0.0028)) keel = [] # 뱃머리 용골선(아래 벽 → 위 끝) N = 12 for i in range(N + 1): s = i / N z = z_low + (z_top + 0.0012 - z_low) * s x = apothem_at(COLLECTOR, z) - 0.0006 + (a_top + 0.0221 - apothem_at(COLLECTOR, z_top)) * s ** 2.2 keel.append(bm.verts.new((x, 0, z))) tip2 = keel[-1] bm.faces.new((tip, r, l)) # 윗면(뚜껑 앞자락) bm.faces.new((tip, l, l2, tip2)) bm.faces.new((tip, tip2, r2, r)) for i in range(N): # 용골에서 양옆 모서리로 펼쳐지는 볼록면 bm.faces.new((keel[i + 1], l2, keel[i])) bm.faces.new((keel[i], r2, keel[i + 1])) bm.faces.new([l, r, r2, keel[0], l2]) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new('Spout') bm.to_mesh(me) bm.free() ob = bpy.data.objects.new('Spout', me) link(ob) assign(ob, ALU) m = ob.modifiers.new('EdgeBevel', 'BEVEL') m.width = 0.0007 m.segments = 2 m.limit_method = 'ANGLE' m.harden_normals = True return obspout()# ---------------------------------------------------------------- 손잡이(-X 면, 검은 수지)def catmull_closed(pts, n): out = [] m = len(pts) for i in range(m): p0, p1, p2, p3 = (Vector(pts[(i + k) % m]) for k in (-1, 0, 1, 2)) for s in range(n): t = s / n t2, t3 = t * t, t * t * t out.append(0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 + (-p0 + 3 * p1 - 3 * p2 + p3) * t3)) return outdef handle(): # (벽에서 바깥으로 거리 u, 높이 z, 고리 폭 w, 두께 t) ctrl = [ (0.004, 0.1700, 0.014, 0.017), (0.028, 0.1765, 0.014, 0.018), (0.052, 0.1750, 0.013, 0.017), (0.061, 0.1640, 0.012, 0.016), (0.056, 0.1400, 0.012, 0.015), (0.043, 0.1150, 0.011, 0.014), (0.026, 0.1030, 0.010, 0.013), (0.011, 0.1080, 0.009, 0.012), (0.005, 0.1400, 0.009, 0.012), ] samples = catmull_closed([(u, z, w, t) for u, z, w, t in ctrl], 16) n = len(samples) bm = bmesh.new() sec = [] for i, s in enumerate(samples): u, z, w, t = s nxt, prv = samples[(i + 1) % n], samples[i - 1] du, dz = nxt[0] - prv[0], nxt[1] - prv[1] L = math.hypot(du, dz) or 1 nu, nz = dz / L, -du / L # 진행 방향의 법선(평면 안) x_wall = -apothem_at(COLLECTOR, z) quad = [] for side, half in ((1, w * 0.75), (-1, w * 0.75)): for yy in (-t * 0.75, t * 0.75): uu = u + side * nu * half zz = z + side * nz * half quad.append(bm.verts.new((x_wall - uu, yy, zz))) sec.append([quad[0], quad[1], quad[3], quad[2]]) for i in range(n): a, b = sec[i], sec[(i + 1) % n] for k in range(4): bm.faces.new((a[k], a[(k + 1) % 4], b[(k + 1) % 4], b[k])) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new('Handle') bm.to_mesh(me) bm.free() ob = bpy.data.objects.new('Handle', me) link(ob) assign(ob, RESIN) subsurf(ob, 2) smooth(ob) return obhandle()# 손잡이를 몸통에 고정하는 알루미늄 브래킷과 뚜껑 경첩a_br = apothem_at(COLLECTOR, 0.166)box('Handle_Bracket', (0.016, 0.020, 0.012), loc=(-a_br - 0.004, 0, 0.160), material=ALU_CAST, base=True)hinge_x = -0.0512 - 0.006box('Lid_Hinge_Tab', (0.014, 0.012, 0.004), loc=(hinge_x + 0.002, 0, LID_Z + 0.0005), material=ALU, base=True)# 엄지로 눌러 뚜껑을 여는 꼬리(뚜껑 뒤쪽에서 비스듬히 솟음)box('Lid_Thumb_Tab', (0.016, 0.014, 0.003), loc=(hinge_x - 0.005, 0, LID_Z + 0.0050), rot=(0, math.radians(28), 0), material=ALU, base=True)cylinder('Lid_Hinge_Pin', r=0.0028, h=0.018, loc=(hinge_x - 0.003, 0, LID_Z + 0.0025), rot=(math.pi / 2, 0, 0), material=ALU_CAST, seg=24, base=False)# ---------------------------------------------------------------- 안전밸브(보일러 -Y 면)zv = 0.046av = apothem_at(BOILER, zv)slope = math.atan2(apothem_at(BOILER, 0.038) - apothem_at(BOILER, 0.056), 0.018)nut = cylinder('Safety_Valve_Nut', r=0.0068, h=0.0045, seg=6, material=STEEL, base=True)cap = cylinder('Safety_Valve_Cap', r=0.0032, h=0.0030, seg=24, material=STEEL, base=True, loc=(0, 0, 0.0045))nut.rotation_euler = (math.pi / 2 - slope, 0, 0)nut.location = (0, -av + 0.0004, zv)off = 0.0045cap.location = (0, -av + 0.0004 - off * math.cos(slope), zv + off * math.sin(slope))cap.rotation_euler = (math.pi / 2 - slope, 0, 0)# ---------------------------------------------------------------- 커피통 옆면(-Y)의 타원 배지(상표 그림은 생략)zb = 0.140ab = apothem_at(COLLECTOR, zb)tilt = math.atan2(apothem_at(COLLECTOR, 0.150) - apothem_at(COLLECTOR, 0.130), 0.020)for nm, sx, sz, d, m in (('Badge_Rim', 0.0078, 0.0100, 0.0009, ALU_CAST), ('Badge_Face', 0.0062, 0.0082, 0.0014, BADGE)): b = cylinder(nm, r=1.0, h=d, seg=48, material=m, base=True) b.scale = (sx, sz, 1.0) b.rotation_euler = (math.pi / 2 + tilt, 0, 0) b.location = (0, -ab + 0.0003, zb)






