
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
타자기는 손으로 글쇠를 눌러 종이에 활자를 찍는 기계다. 초기 타자기는 활자가 플래튼 아래나 뒤를 때려 타자수가 친 글자를 바로 볼 수 없었으나, 1900년 무렵 등장한 언더우드 No.5 계열의 프런트 스트라이크 방식은 활자대가 플래튼 앞면을 때려 찍힌 글자를 즉시 확인할 수 있게 했다. 이 구성은 이후 수동 타자기의 표준 형태가 되었다.
검은 에나멜 몸체 앞쪽에 네 단으로 높아지는 둥근 글쇠가 놓이고, 그 뒤 반원형 바구니 안에 활자대가 부채꼴로 누워 있다. 몸체 위에는 고무 플래튼을 실은 캐리지가 좌우로 움직이며, 한 줄을 다 치면 왼쪽의 리턴 레버로 캐리지를 되돌리고 줄을 바꾼다. 양쪽 리본 스풀 사이를 지나는 잉크 리본이 활자와 종이 사이에 놓인다.
이 모델은 1920년대 사무용 수동 타자기를 실제 크기로 재현했다. QWERTY 배열 글쇠, 42개의 활자대, 플래튼에 감긴 종이와 종이 받침, 금색 장식선과 측면 유리창을 표현했다.
만든 방법
몸체는 측면 프로필을 폭 방향으로 돌출한 뒤 반원과 사각형을 합친 프리즘으로 불리언 차감해 활자대 바구니를 팠다. 글쇠·활자대·세그먼트처럼 반복되는 부품은 bmesh 기본 도형을 행렬 변환으로 한 메시에 누적해 루프로 배치했고, 활자대는 극좌표로 부채꼴을 이루게 했다. 글쇠 글자는 3D 텍스트, 종이와 종이 받침은 곡선 단면을 돌출해 만들었다. 내부의 연결 막대·에스케이프먼트 같은 기구는 생략했고, 상표 데칼은 금색 장식선과 원형 판으로 대신했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 349줄.
# 수동 타자기 (1920년대 프런트 스트라이크형, 언더우드 No.5 계열) — 실제 크기(폭 약 42cm) 모델.# 검은 에나멜 몸체는 측면 프로필을 돌출한 뒤 불리언으로 활자대 바구니 자리를 파냈고,# 반원형으로 펼쳐진 활자대(타이프바), 4단 계단식 둥근 키, 고무 플래튼과 캐리지, 리본 스풀,# 종이 받침과 끼워진 종이를 bmesh 기본 도형(행렬 변환으로 한 메시에 누적)과 루프로 만들었다.# 키 글자는 3D 텍스트로 얹었고, 질감 이미지 없이 Principled BSDF 상수 재질만 썼다.from atelier import *SHOT = {"azimuth": -58, "elevation": 26, "lens": 60, "light": "studio", "background": "#6d7a7e"}# ---------------------------------------------------------------- 재질ENAMEL = mat('Black_Enamel', '#1b1b1d', rough=0.22, coat=0.6)CHROME = mat('Nickel_Chrome', '#c8cbcf', rough=0.14, metal=1.0)STEEL = mat('Typebar_Steel', '#b4b7bb', rough=0.28, metal=1.0)KEYTOP = mat('Key_Celluloid', '#ece3cc', rough=0.3)LETTER = mat('Key_Lettering', '#26221e', rough=0.5)RUBBER = mat('Platen_Rubber', '#2a2828', rough=0.65)GOLD = mat('Gold_Decal', '#c9a24c', rough=0.3, metal=1.0)PAPER = mat('Paper_Sheet', '#eee8d9', rough=0.85)RIBBON = mat('Ink_Ribbon', '#6e1d1f', rough=0.7)FELT = mat('Felt_Pad', '#4d3f33', rough=0.95)GLASS = mat('Side_Glass', '#2c3a3d', rough=0.08)# ---------------------------------------------------------------- bmesh 누적 도우미def M(loc=(0, 0, 0), rot=(0, 0, 0), scale=(1, 1, 1)): return Matrix.LocRotScale(Vector(loc), Euler(rot), Vector(scale))class Parts: """여러 기본 도형을 한 메시에 모은다(오브젝트 수를 줄이고 같은 재질끼리 묶기).""" def __init__(self, name, material): self.name, self.material, self.bm = name, material, bmesh.new() def box(self, loc, size, rot=(0, 0, 0)): bmesh.ops.create_cube(self.bm, size=1.0, matrix=M(loc, rot, size)) def cyl(self, loc, r, h, rot=(0, 0, 0), seg=24, r2=None): bmesh.ops.create_cone(self.bm, cap_ends=True, segments=seg, radius1=r, radius2=r if r2 is None else r2, depth=h, matrix=M(loc, rot)) def rod(self, a, b, r, seg=8): a, b = Vector(a), Vector(b) d = b - a q = d.to_track_quat('Z', 'Y') mtx = Matrix.Translation((a + b) / 2) @ q.to_matrix().to_4x4() bmesh.ops.create_cone(self.bm, cap_ends=True, segments=seg, radius1=r, radius2=r, depth=d.length, matrix=mtx)나머지 301줄 펼치기접기
def strip(self, a, b, width, thick): """a→b 를 잇는 얇은 띠(폭 방향은 수직에 가깝게).""" a, b = Vector(a), Vector(b) d = b - a q = d.to_track_quat('X', 'Z') mtx = Matrix.Translation((a + b) / 2) @ q.to_matrix().to_4x4() @ Matrix.Diagonal((d.length, thick, width, 1)) bmesh.ops.create_cube(self.bm, size=1.0, matrix=mtx) def done(self, smooth_it=False): me = bpy.data.meshes.new(self.name) self.bm.to_mesh(me) self.bm.free() ob = bpy.data.objects.new(self.name, me) link(ob) assign(ob, self.material) if smooth_it: smooth(ob) return obdef yz_profile(name, pts, width, material, x=0.0): """YZ 평면 프로필 [(y,z),...] 을 X 방향으로 width 만큼 돌출.""" bm = bmesh.new() h = width / 2 a = [bm.verts.new((x - h, y, z)) for y, z in pts] b = [bm.verts.new((x + h, y, z)) for y, z in pts] bm.faces.new(a) bm.faces.new(list(reversed(b))) n = len(pts) for i in range(n): bm.faces.new((a[i], a[(i + 1) % n], b[(i + 1) % n], b[i])) 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) return assign(ob, material)# ---------------------------------------------------------------- 주요 치수BODY_W = 0.30 # 몸체 폭BASE_TOP = 0.025 # 받침 윗면DECK = 0.150 # 몸체 윗면PX, PY, PZ = 0.0, 0.050, 0.205 # 플래튼 중심PR = 0.020 # 플래튼 반지름PLATEN_L = 0.33BASKET_C = (0.0, 0.050) # 활자대 바구니 중심(인자점 바로 아래)BASKET_R = 0.118# ---------------------------------------------------------------- 받침과 고무 발base = box('Base_Frame', (BODY_W + 0.012, 0.355, BASE_TOP - 0.006), (0, -0.025, 0.006), material=ENAMEL, base=True)bevel(base, 0.006, 3)feet = Parts('Rubber_Feet', RUBBER)for sx in (-1, 1): for fy in (-0.185, 0.135): feet.cyl((sx * 0.135, fy, 0.003), 0.012, 0.006, seg=20)feet.done(True)# ---------------------------------------------------------------- 몸체 (측면 프로필 돌출 + 바구니 파내기)body_prof = [(-0.108, BASE_TOP - 0.002), (0.150, BASE_TOP - 0.002), (0.150, 0.138), (0.138, DECK), (-0.074, DECK), (-0.090, 0.136), (-0.108, 0.104)]body = yz_profile('Body_Shell', body_prof, BODY_W, ENAMEL)bevel(body, 0.004, 2)# 활자대 바구니: 앞쪽 반원 + 뒤로 이어진 사각형 프리즘을 빼낸다cut_pts = []for i in range(25): a = math.pi + math.pi * i / 24 # 180°→360° (앞쪽 반원) cut_pts.append((BASKET_C[0] + BASKET_R * math.cos(a), BASKET_C[1] + BASKET_R * math.sin(a)))cut_pts += [(BASKET_R, 0.085), (-BASKET_R, 0.085)]cutter = prism('Basket_Cutter', cut_pts, 0.12, loc=(0, 0, 0.100))boolean(body, cutter)# 바구니 바닥(어두운 펠트)floor_pts = [(BASKET_C[0] + (BASKET_R + 0.001) * math.cos(math.pi + math.pi * i / 24), BASKET_C[1] + (BASKET_R + 0.001) * math.sin(math.pi + math.pi * i / 24)) for i in range(25)]floor_pts += [(BASKET_R + 0.001, 0.086), (-BASKET_R - 0.001, 0.086)]prism('Basket_Floor', floor_pts, 0.004, loc=(0, 0, 0.098), material=FELT)# ---------------------------------------------------------------- 활자대(타이프바) 부채꼴seg_parts = Parts('Typebar_Segment', CHROME)seg_r0, seg_r1 = 0.042, 0.050# 반원형 세그먼트(활자대 축받이)for i in range(24): a0 = math.pi + math.pi * i / 24 a1 = math.pi + math.pi * (i + 1) / 24 am = (a0 + a1) / 2 rm = (seg_r0 + seg_r1) / 2 seg_parts.box((BASKET_C[0] + rm * math.cos(am), BASKET_C[1] + rm * math.sin(am), 0.134), (seg_r1 - seg_r0, rm * (a1 - a0) * 1.04, 0.022), (0, 0, am))seg_parts.done()bars = Parts('Typebars', STEEL)slugs = Parts('Type_Slugs', STEEL)N_BARS = 42for i in range(N_BARS): a = math.radians(192 + 156 * i / (N_BARS - 1)) ca, sa = math.cos(a), math.sin(a) r_in, r_out = 0.047, 0.110 z_in, z_out = 0.138, 0.109 p0 = (BASKET_C[0] + r_in * ca, BASKET_C[1] + r_in * sa, z_in) p1 = (BASKET_C[0] + r_out * ca, BASKET_C[1] + r_out * sa, z_out) L = math.dist(p0, p1) slope = math.atan2(z_in - z_out, r_out - r_in) mid = [(u + v) / 2 for u, v in zip(p0, p1)] bars.box(mid, (L, 0.0030, 0.0040), (0, slope, a)) slugs.box((p1[0] - 0.002 * ca, p1[1] - 0.002 * sa, z_out + 0.0035), (0.007, 0.0034, 0.0045), (0, slope, a))bars.done()slugs.done()# 활자대 받침(펠트 반원 링)rest = Parts('Typebar_Rest', FELT)for i in range(20): a = math.radians(190 + 160 * (i + 0.5) / 20) rest.box((BASKET_C[0] + 0.104 * math.cos(a), BASKET_C[1] + 0.104 * math.sin(a), 0.1045), (0.006, 0.104 * math.radians(160 / 20) * 1.05, 0.005), (0, 0, a))rest.done()# ---------------------------------------------------------------- 키보드 (4단 계단식)ROWS = [ # (y, 키 윗면 z, x 오프셋, 글자) (-0.182, 0.058, 0.012, "ZXCVBNM,./"), (-0.160, 0.071, 0.007, "ASDFGHJKL;'"), (-0.138, 0.084, 0.002, "QWERTYUIOP-"), (-0.116, 0.097, -0.003, "23456789_0½"),]PITCH = 0.0195KEY_R = 0.0078rims = Parts('Key_Rims', CHROME)tops = Parts('Key_Tops', KEYTOP)stems = Parts('Key_Levers', ENAMEL)LEVER_Z = 0.036key_positions = []for (ky, kz, xo, chars) in ROWS: n = len(chars) for j, ch in enumerate(chars): kx = xo + (j - (n - 1) / 2) * PITCH key_positions.append((kx, ky, kz, ch))# 시프트·백스페이스 등 가장자리 키extra = [(-0.122, -0.182, 0.058, ''), (0.135, -0.182, 0.058, ''), (-0.124, -0.160, 0.071, ''), (0.118, -0.116, 0.097, '')]key_positions += extrafor kx, ky, kz, ch in key_positions: rims.cyl((kx, ky, kz - 0.0025), KEY_R, 0.005, seg=24) tops.cyl((kx, ky, kz + 0.0003), KEY_R - 0.0012, 0.0006, seg=24) # 키 줄기(아래로) + 키 레버(뒤쪽 몸체로) stems.box((kx, ky + 0.003, (kz - 0.005 + LEVER_Z) / 2), (0.0016, 0.003, kz - 0.005 - LEVER_Z)) stems.box((kx, (ky - 0.108) / 2 + 0.002, LEVER_Z), (0.0022, -0.108 - ky + 0.008, 0.005))rims.done(True)tops.done(True)stems.done()# 키 글자 (3D 텍스트)letters = []for kx, ky, kz, ch in key_positions: if not ch: continue t = text3d('Letter_' + ch, ch, size=0.0085, depth=0.00025, loc=(kx, ky - 0.0028, kz + 0.0009), material=LETTER) letters.append(t)# 스페이스바space = Parts('Space_Bar', ENAMEL)space.box((0.0, -0.203, 0.049), (0.21, 0.009, 0.007))for sx in (-0.085, 0.085): space.box((sx, -0.203, (0.046 + LEVER_Z) / 2), (0.003, 0.003, 0.046 - LEVER_Z)) space.box((sx, (-0.203 - 0.108) / 2, LEVER_Z - 0.004), (0.003, 0.095, 0.004))space.done()bevel(bpy.data.objects['Space_Bar'], 0.002, 2)# 키보드 아래 받침 앞판(레버 가림판)box('Key_Comb', (BODY_W - 0.01, 0.006, 0.010), (0, -0.112, LEVER_Z - 0.003), material=ENAMEL, base=True)# ---------------------------------------------------------------- 캐리지car = Parts('Carriage_Frame', ENAMEL)car.box((0, 0.112, 0.156), (0.40, 0.065, 0.012)) # 캐리지 받침대for sx in (-1, 1): # 플래튼 축을 잡는 끝판 car.box((sx * 0.172, 0.085, 0.188), (0.008, 0.10, 0.052))car.done()bevel(bpy.data.objects['Carriage_Frame'], 0.002, 2)rails = Parts('Carriage_Rails', CHROME)rails.rod((-0.20, 0.140, 0.165), (0.20, 0.140, 0.165), 0.004, seg=12)rails.rod((-0.20, 0.084, 0.165), (0.20, 0.084, 0.165), 0.004, seg=12)rails.box((0, 0.020, 0.176), (0.36, 0.004, 0.012)) # 앞쪽 눈금자rails.done(True)platen = cylinder('Platen', PR, PLATEN_L, (PX - PLATEN_L / 2, PY, PZ), (0, math.pi / 2, 0), RUBBER, seg=48, base=True)smooth(platen)knob = Parts('Platen_Knobs', ENAMEL)for sx in (-1, 1): knob.cyl((sx * 0.1765, PY, PZ), 0.004, 0.006, (0, math.pi / 2, 0), seg=16) knob.cyl((sx * 0.193, PY, PZ), 0.022, 0.026, (0, math.pi / 2, 0), seg=40)knob.done(True)bevel(bpy.data.objects['Platen_Knobs'], 0.003, 2)# 종이 받침(플래튼 뒤 곡면판)tbl = []for i in range(13): t = i / 12 tbl.append((PY + (PR + 0.006) * math.cos(math.radians(60)) + 0.085 * t, PZ + 0.012 + 0.085 * t - 0.015 * t * t))inner = [(y + 0.0, z) for y, z in tbl]outer = [(y + 0.0025 * 0.7, z - 0.0025 * 0.7) for y, z in tbl]paper_table = yz_profile('Paper_Table', inner + list(reversed(outer)), 0.27, ENAMEL)# 끼워진 종이: 인자점 앞에서 플래튼을 감고 종이 받침을 따라 올라간다sheet_pts = []for i in range(14): a = math.radians(200 - 140 * i / 13) # 플래튼 앞-아래(200°)에서 위-뒤(60°)로 sheet_pts.append((PY + (PR + 0.0012) * math.cos(a), PZ + (PR + 0.0012) * math.sin(a)))for y, z in tbl[1:]: sheet_pts.append((y - 0.004, z + 0.004))y_last, z_last = tbl[-1]for k in range(1, 4): sheet_pts.append((y_last - 0.004 + 0.012 * k, z_last + 0.004 + 0.022 * k))sheet_in = sheet_ptssheet_out = [(y, z + 0.0006) for y, z in sheet_pts]paper = yz_profile('Paper_Sheet', sheet_in + list(reversed(sheet_out)), 0.216, PAPER)# 용지 누름대(페이퍼 베일)와 롤러bail = Parts('Paper_Bail', CHROME)bz, by = PZ + PR * 0.75, PY - PR * 0.55bail.rod((-0.16, by - 0.002, bz + 0.003), (0.16, by - 0.002, bz + 0.003), 0.0018, seg=10)for sx in (-1, 1): bail.rod((sx * 0.16, by - 0.002, bz + 0.003), (sx * 0.168, PY + 0.01, PZ + 0.004), 0.0018, seg=8)for rx in (-0.06, 0.06): bail.cyl((rx, by - 0.001, bz + 0.0015), 0.004, 0.012, (0, math.pi / 2, 0), seg=16)bail.done(True)# 캐리지 리턴 레버(왼쪽)lever = Parts('Carriage_Return_Lever', CHROME)lever.rod((-0.176, 0.040, 0.215), (-0.200, 0.010, 0.222), 0.003)lever.rod((-0.200, 0.010, 0.222), (-0.225, -0.040, 0.232), 0.003)lever.box((-0.230, -0.052, 0.234), (0.016, 0.026, 0.004), (0.15, 0, -0.35))lever.done(True)# ---------------------------------------------------------------- 리본 스풀과 리본spools = Parts('Ribbon_Spool_Cups', CHROME)ribs = Parts('Ribbon_Rolls', RIBBON)for sx in (-1, 1): spools.cyl((sx * 0.124, -0.035, DECK + 0.002), 0.024, 0.004, seg=40) ribs.cyl((sx * 0.124, -0.035, DECK + 0.008), 0.019, 0.008, seg=40) spools.cyl((sx * 0.124, -0.035, DECK + 0.014), 0.004, 0.006, seg=16)spools.done(True)ribs.done(True)# 리본 진동판(가운데 리본 가이드)guide = Parts('Ribbon_Vibrator', CHROME)guide.box((0, PY - PR - 0.004, PZ - 0.012), (0.016, 0.002, 0.012))guide.done()# ---------------------------------------------------------------- 금색 줄무늬gold = Parts('Gold_Pinstripes', GOLD)gold.box((0, 0.1505, 0.110), (0.26, 0.0012, 0.0025))gold.done()# 리본 띠: 양쪽 스풀에서 가운데 리본 가이드로band = Parts('Ribbon_Band', RIBBON)for sx in (-1, 1): band.strip((sx * 0.106, -0.024, DECK + 0.009), (sx * 0.035, 0.012, PZ - 0.020), 0.010, 0.0008) band.strip((sx * 0.035, 0.012, PZ - 0.020), (sx * 0.008, PY - PR - 0.0055, PZ - 0.012), 0.010, 0.0008)band.done()# 측면 유리창(양옆)과 금색 테두리for sx in (-1, 1): xs = sx * (BODY_W / 2 + 0.0007) win = [(-0.040, 0.050), (0.120, 0.050), (0.120, 0.125), (-0.040, 0.125)] yz_profile('Side_Window_' + ('L' if sx < 0 else 'R'), win, 0.0014, GLASS, x=xs) fr = Parts('Side_Window_Trim_' + ('L' if sx < 0 else 'R'), GOLD) xf = sx * (BODY_W / 2 + 0.0016) for (y0, z0), (y1, z1) in zip(win, win[1:] + win[:1]): fr.rod((xf, y0, z0), (xf, y1, z1), 0.0012, seg=6) fr.done()# 앞면 경사판의 금색 장식선과 가운데 상표판 자리front_gold = Parts('Front_Gold_Decal', GOLD)slope = math.atan2(0.136 - 0.104, -0.090 + 0.108) # 경사판 기울기nrm = Vector((0, -math.sin(slope), math.cos(slope)))for t in (0.18, 0.82): c = Vector((0, -0.108 + 0.018 * t, 0.104 + 0.032 * t)) + nrm * 0.0007 front_gold.box(c, (0.25, 0.0012, 0.0015), (math.pi / 2 + slope, 0, 0))cen = Vector((0, -0.099, 0.120)) + nrm * 0.0008front_gold.cyl(cen, 0.008, 0.0012, (slope, 0, 0), seg=32)front_gold.done()# 뒷면 메인스프링 드럼(캐리지를 당기는 태엽통)drum = Parts('Mainspring_Drum', ENAMEL)drum.cyl((-0.100, 0.1535, 0.090), 0.030, 0.008, (math.pi / 2, 0, 0), seg=40)drum.cyl((-0.100, 0.1590, 0.090), 0.006, 0.004, (math.pi / 2, 0, 0), seg=16)drum.done(True)# 여백 조절 막대(캐리지 뒤 위)와 여백 멈추개, 용지 해제 레버, 줄 간격 래칫mr = Parts('Margin_Rack', CHROME)mr.box((0, 0.136, 0.176), (0.36, 0.006, 0.010))for mx in (-0.11, 0.12): mr.box((mx, 0.136, 0.184), (0.010, 0.010, 0.008))mr.rod((0.172, 0.105, 0.208), (0.170, 0.128, 0.240), 0.0022) # 용지 해제 레버(오른쪽)mr.box((0.170, 0.130, 0.243), (0.006, 0.012, 0.006))mr.cyl((-0.1775, PY, PZ), 0.014, 0.003, (0, math.pi / 2, 0), seg=30) # 줄 간격 래칫 휠mr.done(True)






