
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
그라모폰은 에밀 베를리너가 1887년에 특허를 낸 평원반 음반 재생기다. 원통형 음반을 쓰던 에디슨의 포노그래프와 달리 납작한 원반을 써서 대량 복제가 쉬웠고, 1900~1910년대에는 태엽 모터와 커다란 외부 나팔을 갖춘 형태가 가정에 널리 보급되었다.
전기 증폭이 없던 시절이라 소리는 순전히 기계적으로 만들어졌다. 바늘이 음반의 소리골을 따라 떨리면 사운드박스의 얇은 운모 진동판이 울리고, 그 진동이 톤암과 나팔을 지나며 커진다. 나팔이 끝으로 갈수록 급하게 벌어지는 것은 좁은 관 속 공기의 진동을 넓은 공간으로 효율적으로 내보내기 위해서다.
이 모델은 마호가니 캐비닛 위에 셸락 78회전 음반을 올리고, 뒤쪽 톤암 받침에서 솟아 앞으로 휘어 나오는 꽃잎형 놋쇠 나팔을 단 모습을 표현했다. 오른쪽 면의 태엽 크랭크, 니켈 도금 톤암과 사운드박스, 나팔을 받치는 뒤쪽 지지대까지 실제 크기에 맞춰 배치했다.
만든 방법
나팔은 3차 베지어 곡선 위에 평행 이동 프레임을 세우고 단면 원을 쓸어(sweep) 만들었으며, 끝으로 갈수록 급해지는 반지름 함수와 각도에 따른 굴곡으로 10장의 꽃잎 판을 표현했다. 바깥면과 안쪽면을 이어 두께를 주고, 이음매 리브와 말린 테두리는 같은 곡선 위의 점을 따라 관(curve tube)으로 붙였다. 음반은 회전체(lathe) 단면에 0.8mm 간격의 미세한 홈을 넣어 반사광 줄무늬가 생기게 했다. 캐비닛은 상자와 몰딩, 돋을 패널, 회전체 모서리 기둥을 쌓아 만들었다. 태엽 모터와 내부 기구, 음반 라벨의 글자, 나무결은 생략했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 334줄.
# 나팔 축음기 (1900~1910년대 태엽식 디스크 그라모폰)# - 놋쇠 나팔: 3차 베지어 곡선을 따라 단면 원을 평행 이동 프레임으로 쓸어(sweep) 만들었다.# 반지름은 끝으로 갈수록 급하게 벌어지고, 10장의 꽃잎 판과 이음매 리브, 말린 테두리를 수식으로 붙였다.# - 마호가니 캐비닛: 상자·몰딩·패널·모서리 기둥을 겹쳐 쌓았다.# - 78회전 셸락 음반: 회전체(lathe) 단면에 미세한 홈 굴곡을 넣어 빛 줄무늬가 생기게 했다.# - 톤암·사운드박스·태엽 크랭크·나팔 지지대(크레인)를 실제 비례(캐비닛 약 34cm, 나팔 지름 약 56cm)로 만들었다.from atelier import *SHOT = {"azimuth": -34, "elevation": 20, "lens": 60, "light": "studio", "background": "#27313d"}# ---------------------------------------------------------------- 재질M_WOOD = mat('Mahogany', '#5b2c19', 0.32, 0.0, coat=0.6)M_WOOD_D = mat('Mahogany_Dark', '#3e1d10', 0.4, 0.0, coat=0.4)M_BRASS = mat('Brass_Polished', '#d8b263', 0.2, 1.0)M_BRASS_IN = mat('Brass_Inner', '#c29a50', 0.3, 1.0)M_NICKEL = mat('Nickel_Plate', '#c6c4bd', 0.18, 1.0)M_SHELLAC = mat('Shellac_Record', '#17161a', 0.22, 0.0)M_LABEL = mat('Record_Label', '#7c2128', 0.5, 0.0)M_LABEL_G = mat('Label_Gold', '#c8a85a', 0.35, 0.6)M_FELT = mat('Turntable_Felt', '#2d5236', 0.95, 0.0)M_MICA = mat('Mica_Diaphragm', '#4b3f2f', 0.15, 0.0)M_HANDLE = mat('Crank_Handle_Ebony', '#2b1a12', 0.35, 0.0)M_STEEL = mat('Blued_Steel', '#3a3d44', 0.3, 1.0)# ---------------------------------------------------------------- 도우미def bezier(p0, p1, p2, p3, t): u = 1 - t return p0 * (u ** 3) + p1 * (3 * u * u * t) + p2 * (3 * u * t * t) + p3 * (t ** 3)def frames(points): """평행 이동(parallel transport) 프레임: 각 점의 (T, N, B).""" n = len(points) T = [] for i in range(n): a = points[max(i - 1, 0)] b = points[min(i + 1, n - 1)] T.append((b - a).normalized()) ref = Vector((1, 0, 0)) if abs(T[0].x) < 0.9 else Vector((0, 1, 0)) N = [(ref - T[0] * ref.dot(T[0])).normalized()] for i in range(1, n): v = N[-1] - T[i] * N[-1].dot(T[i]) N.append(v.normalized()) B = [T[i].cross(N[i]) for i in range(n)] return T, N, B나머지 286줄 펼치기접기
def sweep(name, points, radius_fn, seg=32, material=None, cap_start=True, cap_end=True, theta0=0.0): """points 를 따라 단면 원(radius_fn(i, theta))을 쓸어 닫힌 관을 만든다.""" T, N, B = frames(points) bm = bmesh.new() rings = [] for i, p in enumerate(points): ring = [] for j in range(seg): th = theta0 + 2 * math.pi * j / seg r = radius_fn(i, th) ring.append(bm.verts.new(p + (N[i] * math.cos(th) + B[i] * math.sin(th)) * r)) rings.append(ring) for i in range(len(points) - 1): for j in range(seg): k = (j + 1) % seg bm.faces.new((rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j])) if cap_start: bm.faces.new(list(reversed(rings[0]))) if cap_end: 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) return smooth(ob), (T, N, B)def aim_rot(d): """로컬 +Z 를 방향 d 로 돌리는 오일러.""" return Vector((0, 0, 1)).rotation_difference(Vector(d).normalized()).to_euler()def rod(name, a, b, r, material, seg=16): a, b = Vector(a), Vector(b) d = b - a return cylinder(name, r, d.length, loc=a, rot=aim_rot(d), material=material, seg=seg)# ---------------------------------------------------------------- 캐비닛W = 0.30 # 몸통 폭FOOT_H = 0.016PLINTH = (0.015 + 0.001, 0.036)BODY_Z0, BODY_Z1 = 0.036, 0.152LID_Z0, LID_Z1 = 0.152, 0.170TOP = LID_Z1for i, (sx, sy) in enumerate(((1, 1), (-1, 1), (1, -1), (-1, -1))): f = lathe(f'Cabinet_Foot_{i}', [(0.0, 0.0), (0.016, 0.0), (0.021, 0.006), (0.019, 0.013), (0.014, FOOT_H), (0.0, FOOT_H)], loc=(sx * 0.145, sy * 0.145, 0), material=M_WOOD_D, seg=24)plinth = box('Cabinet_Plinth', (0.344, 0.344, BODY_Z0 - FOOT_H), loc=(0, 0, FOOT_H), base=True, material=M_WOOD_D)bevel(plinth, 0.006, 2)plinth_mold = box('Cabinet_Plinth_Molding', (0.33, 0.33, 0.008), loc=(0, 0, BODY_Z0), base=True, material=M_WOOD)bevel(plinth_mold, 0.004, 3)body = box('Cabinet_Body', (W, W, BODY_Z1 - BODY_Z0), loc=(0, 0, BODY_Z0), base=True, material=M_WOOD)bevel(body, 0.002, 1)lid_mold = box('Cabinet_Lid_Molding', (0.322, 0.322, 0.007), loc=(0, 0, LID_Z0 - 0.004), base=True, material=M_WOOD)bevel(lid_mold, 0.003, 3)lid = box('Cabinet_Lid', (0.342, 0.342, LID_Z1 - LID_Z0 - 0.003), loc=(0, 0, LID_Z0 + 0.003), base=True, material=M_WOOD_D)bevel(lid, 0.005, 3)# 네 면의 돋을 패널과 모서리 기둥PANEL_H = BODY_Z1 - BODY_Z0 - 0.038panel_z = (BODY_Z0 + BODY_Z1) / 2for k, ang in enumerate((0, 90, 180, 270)): a = math.radians(ang) nx, ny = math.sin(a), -math.cos(a) # ang=0 → 정면(-Y) frame = box(f'Cabinet_Panel_Frame_{k}', (0.226, 0.006, PANEL_H + 0.014), loc=(nx * (W / 2 + 0.002), ny * (W / 2 + 0.002), panel_z), rot=(0, 0, a), material=M_WOOD_D) bevel(frame, 0.0025, 2) pan = box(f'Cabinet_Panel_{k}', (0.204, 0.006, PANEL_H - 0.006), loc=(nx * (W / 2 + 0.0045), ny * (W / 2 + 0.0045), panel_z), rot=(0, 0, a), material=M_WOOD) bevel(pan, 0.004, 3)col_prof = [(0.0, BODY_Z0), (0.016, BODY_Z0), (0.016, BODY_Z0 + 0.008), (0.012, BODY_Z0 + 0.012), (0.011, BODY_Z1 - 0.014), (0.0135, BODY_Z1 - 0.009), (0.016, BODY_Z1 - 0.004), (0.016, BODY_Z1), (0.0, BODY_Z1)]corner_col = lathe('Cabinet_Corner_Column_0', col_prof, loc=(W / 2, W / 2, 0), material=M_WOOD_D, seg=20)for i, (sx, sy) in enumerate(((-1, 1), (1, -1), (-1, -1))): duplicate(corner_col, loc=(sx * W / 2, sy * W / 2, 0), name=f'Cabinet_Corner_Column_{i + 1}')# 정면 놋쇠 명판plate = box('Front_Nameplate', (0.06, 0.003, 0.016), loc=(0, -(W / 2 + 0.0075 + 0.0012), panel_z), material=M_BRASS)bevel(plate, 0.001, 1)# ---------------------------------------------------------------- 턴테이블과 음반CX, CY = -0.005, -0.012hub = cylinder('Turntable_Hub', 0.03, 0.006, loc=(CX, CY, TOP), material=M_NICKEL, seg=32)PL_Z0 = TOP + 0.006platter = lathe('Turntable_Platter', [(0.0, PL_Z0), (0.128, PL_Z0), (0.131, PL_Z0 + 0.003), (0.131, PL_Z0 + 0.009), (0.129, PL_Z0 + 0.010), (0.0, PL_Z0 + 0.010)], loc=(CX, CY, 0), material=M_STEEL, seg=96)FELT_Z = PL_Z0 + 0.010felt = cylinder('Turntable_Felt', 0.1285, 0.0015, loc=(CX, CY, FELT_Z), material=M_FELT, seg=96)REC_Z0 = FELT_Z + 0.0015REC_T = 0.0022REC_TOP = REC_Z0 + REC_Tprof = [(0.0036, REC_Z0), (0.124, REC_Z0), (0.1268, REC_Z0 + 0.0004), (0.1272, REC_Z0 + REC_T * 0.5), (0.1265, REC_TOP), (0.121, REC_TOP)]r = 0.121while r > 0.050: # 소리골: 미세한 V 홈 prof.append((r - 0.0004, REC_TOP - 0.00018)) prof.append((r - 0.0008, REC_TOP)) r -= 0.0008prof += [(0.047, REC_TOP), (0.0036, REC_TOP), (0.0036, REC_Z0)]record = lathe('Shellac_Record_78rpm', prof, loc=(CX, CY, 0), material=M_SHELLAC, seg=128)label = cylinder('Record_Label', 0.044, 0.0004, loc=(CX, CY, REC_TOP), material=M_LABEL, seg=64)label_ring = torus('Record_Label_Gold_Ring', 0.038, 0.0007, loc=(CX, CY, REC_TOP + 0.0004), material=M_LABEL_G, seg=64, rseg=6)label_ring.scale.z = 0.4spindle = lathe('Turntable_Spindle', [(0.0, REC_Z0 - 0.001), (0.0036, REC_Z0 - 0.001), (0.0036, REC_TOP + 0.010), (0.0025, REC_TOP + 0.013), (0.0, REC_TOP + 0.0135)], loc=(CX, CY, 0), material=M_NICKEL, seg=20)# ---------------------------------------------------------------- 톤암 받침, 톤암, 사운드박스BASE = Vector((0.118, 0.122, TOP))ARM_Z = REC_TOP + 0.045base_prof = [(0.0, 0.0), (0.034, 0.0), (0.034, 0.004), (0.026, 0.008), (0.022, 0.012), (0.021, ARM_Z - TOP - 0.012), (0.026, ARM_Z - TOP - 0.008), (0.026, ARM_Z - TOP + 0.012), (0.022, ARM_Z - TOP + 0.016), (0.0, ARM_Z - TOP + 0.016)]tone_base = lathe('Tonearm_Base', base_prof, loc=BASE, material=M_NICKEL, seg=40)HORN_Z0 = BASE.z + ARM_Z - TOP + 0.016# 바늘 끝이 음반 반지름 약 9cm 지점에 오도록needle_tip = Vector((CX + 0.062, CY - 0.062, REC_TOP))arm_dir = (needle_tip - Vector((BASE.x, BASE.y, needle_tip.z)))arm_dir.z = 0arm_dir.normalize()SB_C = Vector((needle_tip.x, needle_tip.y, ARM_Z)) - arm_dir * 0.004ARM_END = SB_C - arm_dir * 0.032arm_start = Vector((BASE.x, BASE.y, ARM_Z)) + arm_dir * 0.018arm_pts = [arm_start.lerp(ARM_END - Vector((0, 0, 0.0)), i / 20) for i in range(21)]# 톤암은 완만하게 위로 휘었다 내려온다for i, p in enumerate(arm_pts): p.z += 0.010 * math.sin(math.pi * i / 20)tonearm, _ = sweep('Tonearm_Tube', arm_pts, lambda i, th: 0.0105 - 0.0035 * i / 20, seg=20, material=M_NICKEL)rod('Tonearm_Root', Vector((BASE.x, BASE.y, ARM_Z)), arm_start + arm_dir * 0.002, 0.0108, M_NICKEL, seg=20)neck = rod('Soundbox_Neck', ARM_END - arm_dir * 0.004, SB_C - arm_dir * 0.006, 0.0075, M_NICKEL, seg=16)SB_R = 0.031sb_rot = aim_rot(arm_dir)sb_shell = cylinder('Soundbox_Body', SB_R, 0.012, loc=SB_C - arm_dir * 0.007, rot=sb_rot, material=M_NICKEL, seg=40)bevel(sb_shell, 0.002, 2)sb_rim = torus('Soundbox_Front_Rim', SB_R - 0.002, 0.0028, loc=SB_C + arm_dir * 0.005, rot=sb_rot, material=M_NICKEL, seg=48, rseg=10)sb_mica = cylinder('Soundbox_Mica_Diaphragm', SB_R - 0.004, 0.001, loc=SB_C + arm_dir * 0.004, rot=sb_rot, material=M_MICA, seg=40)# 스타일러스 바: 진동판 중심 → 아래 바늘 집게side = arm_dir.cross(Vector((0, 0, 1))).normalized()clamp_c = SB_C + arm_dir * 0.009 + Vector((0, 0, -SB_R - 0.004))rod('Soundbox_Stylus_Bar', SB_C + arm_dir * 0.006, clamp_c + Vector((0, 0, 0.003)), 0.0016, M_NICKEL, seg=10)nh = box('Soundbox_Needle_Clamp', (0.007, 0.007, 0.012), loc=clamp_c, rot=sb_rot.copy(), material=M_NICKEL)nh.rotation_euler = aim_rot(Vector((0, 0, 1)))nh.rotation_euler.z = math.atan2(arm_dir.y, arm_dir.x)cone('Steel_Needle', 0.0011, clamp_c.z - 0.006 - REC_TOP + 0.0002, loc=Vector((clamp_c.x, clamp_c.y, clamp_c.z - 0.006)), rot=(math.pi, 0, 0), material=M_STEEL, seg=10)for s in (-1, 1): sp = cylinder(f'Soundbox_Screw_{"L" if s < 0 else "R"}', 0.0025, 0.004, loc=SB_C + side * s * 0.018 + arm_dir * 0.006, rot=sb_rot, material=M_BRASS, seg=10)# ---------------------------------------------------------------- 놋쇠 나팔H0 = Vector((BASE.x, BASE.y, HORN_Z0))BELL = Vector((-0.02, -0.17, 0.56))BELL_AX = Vector((0.0, -0.90, 0.44)).normalized()P1 = H0 + Vector((0, 0.03, 0.19))P2 = BELL - BELL_AX * 0.26NS = 90horn_pts = [bezier(H0, P1, P2, BELL, i / (NS - 1)) for i in range(NS)]R0, R1 = 0.0215, 0.28PETALS = 10def horn_r(t): return R0 + 0.045 * t + (R1 - R0 - 0.045) * t ** 3.4def horn_outer(i, th): t = i / (NS - 1) bulge = abs(math.sin(PETALS * th / 2)) ** 0.7 - 0.55 return horn_r(t) * (1 + 0.055 * t ** 2.5 * bulge)THK = 0.0016# 바깥면 + 안쪽면을 이어 두께가 있는 껍데기로T, N, B = frames(horn_pts)bm = bmesh.new()SEG = 120outer, inner = [], []for i, p in enumerate(horn_pts): ro, ri = [], [] for j in range(SEG): th = 2 * math.pi * j / SEG d = N[i] * math.cos(th) + B[i] * math.sin(th) r = horn_outer(i, th) ro.append(bm.verts.new(p + d * r)) ri.append(bm.verts.new(p + d * (r - THK))) outer.append(ro) inner.append(ri)bell_inner_faces = []for i in range(NS - 1): for j in range(SEG): k = (j + 1) % SEG bm.faces.new((outer[i][j], outer[i][k], outer[i + 1][k], outer[i + 1][j])) f = bm.faces.new((inner[i][j], inner[i + 1][j], inner[i + 1][k], inner[i][k])) bell_inner_faces.append(f)for j in range(SEG): k = (j + 1) % SEG bm.faces.new((outer[0][j], inner[0][j], inner[0][k], outer[0][k])) bm.faces.new((outer[-1][j], outer[-1][k], inner[-1][k], inner[-1][j]))for f in bell_inner_faces: f.material_index = 1me = bpy.data.meshes.new('Brass_Horn')bm.to_mesh(me)bm.free()horn = bpy.data.objects.new('Brass_Horn', me)link(horn)horn.data.materials.append(M_BRASS)horn.data.materials.append(M_BRASS_IN)smooth(horn)# 꽃잎 이음매 리브(바깥 홈 위)for k in range(PETALS): th = 2 * math.pi * k / PETALS pts = [] for i in range(int(NS * 0.25), NS): d = N[i] * math.cos(th) + B[i] * math.sin(th) pts.append(tuple(horn_pts[i] + d * (horn_outer(i, th) + 0.0006))) curve_tube(f'Horn_Seam_Rib_{k}', pts, radius=0.0016, material=M_BRASS, resolution=2, bevel_res=2)# 말린 테두리lip = []for j in range(SEG): th = 2 * math.pi * j / SEG d = N[-1] * math.cos(th) + B[-1] * math.sin(th) lip.append(tuple(horn_pts[-1] + d * (horn_outer(NS - 1, th) - THK / 2)))curve_tube('Horn_Rolled_Lip', lip, radius=0.0042, material=M_BRASS, closed=True, resolution=2, bevel_res=3)# 나팔 목의 연결 칼라collar = lathe('Horn_Throat_Collar', [(0.0, -0.002), (0.025, -0.002), (0.027, 0.004), (0.027, 0.016), (0.024, 0.022), (0.0, 0.022)], loc=H0, material=M_BRASS, seg=40)# ---------------------------------------------------------------- 나팔 지지대(크레인)ic = int(NS * 0.55)C = horn_pts[ic]rc = horn_r(ic / (NS - 1)) * 1.03 + 0.004clamp = torus('Horn_Crane_Clamp', rc, 0.0045, loc=C, rot=aim_rot(T[ic]), material=M_BRASS, seg=48, rseg=10)back = Vector((0, 1, 0))back = (back - T[ic] * back.dot(T[ic])).normalized()Q = C + back * rcBACK_Y = W / 2 + 0.006post_x = Q.xfoot = box('Horn_Crane_Mount', (0.03, 0.012, 0.07), loc=(post_x, W / 2 + 0.0075, panel_z), material=M_BRASS)bevel(foot, 0.0015, 1)crane_pts = [(post_x, BACK_Y + 0.016, panel_z - 0.02), (post_x, BACK_Y + 0.03, panel_z + 0.01), (post_x, BACK_Y + 0.03, TOP + 0.05)]mid = Vector(crane_pts[-1]).lerp(Q, 0.5) + Vector((0, 0.05, 0))for s in range(1, 9): tt = s / 8 a = Vector(crane_pts[2]).lerp(mid, tt) b = mid.lerp(Q, tt) crane_pts.append(tuple(a.lerp(b, tt)))curve_tube('Horn_Crane_Arm', crane_pts, radius=0.0065, material=M_BRASS, resolution=4, bevel_res=3)rod('Horn_Crane_Bracket_Stub', (post_x, BACK_Y + 0.006, panel_z - 0.02), (post_x, BACK_Y + 0.02, panel_z - 0.02), 0.0055, M_BRASS)# ---------------------------------------------------------------- 태엽 크랭크(오른쪽 면)CR = Vector((W / 2 + 0.0075, -0.02, panel_z - 0.004))esc = cylinder('Crank_Escutcheon', 0.014, 0.003, loc=CR, rot=(0, math.pi / 2, 0), material=M_NICKEL, seg=32)bevel(esc, 0.001, 1)shaft_end = CR + Vector((0.036, 0, 0))rod('Crank_Shaft', CR, shaft_end, 0.0042, M_NICKEL)arm_v = Vector((0, -0.55, -0.83)).normalized() * 0.085crank_pts = [tuple(shaft_end + Vector((0.0, 0, 0))), tuple(shaft_end + arm_v * 0.35 + Vector((0.006, 0, 0))), tuple(shaft_end + arm_v * 0.75 + Vector((0.004, 0, 0))), tuple(shaft_end + arm_v)]curve_tube('Crank_Arm', crank_pts, radius=0.0045, material=M_NICKEL, resolution=6)boss = sphere('Crank_Boss', 0.0065, loc=shaft_end, material=M_NICKEL, seg=16, rings=8)hend = shaft_end + arm_vrod('Crank_Handle_Pin', hend, hend + Vector((0.05, 0, 0)), 0.0025, M_NICKEL)lathe('Crank_Handle_Grip', [(0.0, 0.004), (0.006, 0.004), (0.0085, 0.012), (0.009, 0.03), (0.0075, 0.042), (0.004, 0.046), (0.0, 0.046)], loc=hend, rot=(0, math.pi / 2, 0), material=M_HANDLE, seg=20)# ---------------------------------------------------------------- 속도 조절기·브레이크(윗면 앞쪽 오른쪽)SP = Vector((0.142, -0.142, TOP))cylinder('Speed_Dial', 0.012, 0.002, loc=SP, material=M_NICKEL, seg=32)rod('Speed_Lever', SP + Vector((0, 0, 0.002)), SP + Vector((-0.02, 0.012, 0.0045)), 0.0018, M_NICKEL, seg=10)BR = Vector((0.142, 0.04, TOP))cylinder('Brake_Pivot', 0.005, 0.005, loc=BR, material=M_NICKEL, seg=16)rod('Brake_Lever', BR + Vector((0, 0, 0.004)), BR + Vector((-0.004, -0.026, 0.006)), 0.0018, M_NICKEL, seg=10)






