본문으로 건너뛰기
3D 아틀리에
콘서트 그랜드 피아노 3D 모형

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

이 모형에 대하여

그랜드 피아노는 줄과 향판을 수평으로 눕힌 피아노다. 18세기 초 이탈리아의 바르톨로메오 크리스토포리가 해머로 줄을 치는 방식을 고안한 뒤, 19세기에 주철 프레임과 교차 현(overstringing)이 도입되면서 오늘날의 형태가 자리 잡았다. 길이 2.7m 안팎의 콘서트 그랜드는 대부분의 연주회장에서 쓰인다.

긴 저음 현과 짧은 고음 현의 길이 차이 때문에 케이스 한쪽은 곧고 다른 쪽은 안으로 휘어 들어가는 곡선(벤트사이드)을 이루며, 위에서 보면 날개 모양이 된다. 연주 때는 곧은 쪽에 경첩이 달린 뚜껑을 받침대로 열어 소리를 객석 쪽으로 반사시킨다.

이 모델은 뚜껑을 받침대로 연 연주 상태의 콘서트 그랜드를 실제 치수로 재현했다. 흑단색 도장 케이스, 52개의 흰 건반과 36개의 검은 건반, 보면대, 금색 철골 프레임과 그 아래의 향판, 고음부 세 줄과 저음부 동선 교차 현, 튜닝핀과 댐퍼, 세 개의 다리와 황동 캐스터, 리라와 세 개의 페달을 담았다.

만든 방법

케이스 윤곽은 벤트사이드 제어점 15개를 캐트멀-롬 곡선으로 보간한 다각형이며, 이를 안쪽으로 오프셋해 림 벽·향판·라이너·철골 프레임을, 바깥으로 오프셋해 뚜껑을 만들었다. 프레임의 큰 개구부는 같은 윤곽을 더 깊게 오프셋한 프리즘으로 불리언 차감했고, 방사형 지지대는 상자를 회전 배치했다. 건반은 A0 부터 음이름 규칙(A·C·D·F·G 다음에 검은 건반)으로 88개를 배열했고, 줄·튜닝핀·댐퍼는 메시를 공유하는 복제본으로 200여 개를 놓았다. 다리와 캐스터는 회전체, 리라는 곡선 관으로 만들었다. 실제 액션(해머 기구)과 프레임의 세부 주조 문양, 줄 수(실제 약 230개)는 단순화했다.

build.py

이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 367줄.

# 콘서트 그랜드 피아노 (길이 약 2.74m급, 뚜껑을 받침대로 연 연주 상태)
# - 날개 모양 케이스 윤곽은 제어점을 캐트멀-롬 곡선으로 보간해 얻고, 그 다각형을 안쪽으로 오프셋해
# 림(옆판) 벽, 향판, 철골 프레임, 뚜껑을 모두 같은 윤곽에서 만들었다.
# - 건반 88개(흰 건반 52 + 검은 건반 36)는 A0 부터 음이름 규칙으로 배열했고,
# 줄·튜닝핀·댐퍼는 메시를 공유하는 복제본을 수학적으로 배치했다(고음 3줄, 저음 동선 교차).
# - 다리·캐스터는 회전체(lathe), 리라와 페달은 상자·관으로 조립했다. 재질은 상수 색 Principled BSDF.
import math
from atelier import *
SHOT = {"azimuth": -48, "elevation": 26, "lens": 55, "light": "studio", "background": "#7d8894"}
# ---------------------------------------------------------------- 재질
EBONY = mat('Ebony_Lacquer', '#121215', rough=0.12, coat=1.0)
IVORY = mat('Key_Ivory', '#ece6d6', rough=0.3)
KEYBLK = mat('Key_Ebony', '#1b1a1c', rough=0.35)
GOLD = mat('Iron_Plate_Gold', '#c4923a', rough=0.4, metal=0.85)
SPRUCE = mat('Soundboard_Spruce', '#e6cb98', rough=0.5)
MAPLE = mat('Bridge_Maple', '#9c6b3f', rough=0.5)
STEEL = mat('String_Steel', '#d5d8dc', rough=0.18, metal=1.0)
COPPER = mat('Bass_String_Copper', '#b4703c', rough=0.3, metal=1.0)
BRASS = mat('Brass', '#c9a14c', rough=0.2, metal=1.0)
FELT_R = mat('Felt_Red', '#7e1b22', rough=0.9)
FELT_K = mat('Damper_Felt', '#2b2a2d', rough=0.85)
# ---------------------------------------------------------------- 치수
W = 1.57 # 케이스 폭
HW = W / 2
Z_BOT = 0.62 # 케이스 바닥(림 아래)
Z_TOP = 0.98 # 림 윗면
Y_FRONT = 0.30 # 림 앞벽 (건반 뒤)
RIM_T = 0.045 # 림 두께
CHEEK_X = 0.611 # 건반 양끝(치크 블록 안쪽 면)
# 벤트사이드(오른쪽 곡선 옆판)~꼬리 제어점 (x, y)
BENT = [(HW, 0.0), (HW, 0.55), (0.772, 0.78), (0.715, 1.00), (0.605, 1.22), (0.468, 1.45),
(0.338, 1.68), (0.226, 1.92), (0.132, 2.15), (0.035, 2.38), (-0.10, 2.57),
(-0.30, 2.70), (-0.52, 2.745), (-0.70, 2.73), (-HW, 2.67)]
def catmull(pts, n=10):
out = []
P = [pts[0]] + pts + [pts[-1]]
for i in range(1, len(P) - 2):
p0, p1, p2, p3 = P[i - 1], P[i], P[i + 1], P[i + 2]
for k in range(n):
t = k / n
t2, t3 = t * t, t * t * t
out.append(tuple(0.5 * ((2 * p1[j]) + (-p0[j] + p2[j]) * t + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * t2
나머지 319줄 펼치기
+ (-p0[j] + 3 * p1[j] - 3 * p2[j] + p3[j]) * t3) for j in range(2)))
out.append(pts[-1])
return out
BENT_DENSE = catmull(BENT, 12)
def outline(front_y=Y_FRONT):
"""앞변이 front_y 인 반시계 방향 케이스 윤곽."""
pts = [(-HW, front_y), (HW, front_y)]
for x, y in BENT_DENSE:
if y > front_y + 0.02:
pts.append((x, y))
pts.append((-HW, 2.55))
pts.append((-HW, 1.5))
pts.append((-HW, 0.9))
return pts
def offset(pts, d):
"""반시계 다각형을 안쪽으로 d 만큼 (미터 접합)."""
n = len(pts)
out = []
for i in range(n):
p0, p1, p2 = Vector((*pts[i - 1], 0)), Vector((*pts[i], 0)), Vector((*pts[(i + 1) % n], 0))
e1 = (p1 - p0).normalized()
e2 = (p2 - p1).normalized()
n1 = Vector((-e1.y, e1.x, 0))
n2 = Vector((-e2.y, e2.x, 0))
m = (n1 + n2).normalized()
c = max(m.dot(n1), 0.3)
q = p1 + m * (d / c)
out.append((q.x, q.y))
return out
def sharpen(ob, deg=35):
smooth(ob)
ob.data.set_sharp_from_angle(angle=math.radians(deg))
return ob
def ring_wall(name, outer, inner, z0, z1, material):
bm = bmesh.new()
n = len(outer)
ob_ = [bm.verts.new((x, y, z0)) for x, y in outer]
ot = [bm.verts.new((x, y, z1)) for x, y in outer]
ib = [bm.verts.new((x, y, z0)) for x, y in inner]
it = [bm.verts.new((x, y, z1)) for x, y in inner]
for i in range(n):
j = (i + 1) % n
bm.faces.new((ob_[i], ob_[j], ot[j], ot[i]))
bm.faces.new((it[i], it[j], ib[j], ib[i]))
bm.faces.new((ot[i], ot[j], it[j], it[i]))
bm.faces.new((ib[i], ib[j], ob_[j], ob_[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)
assign(ob, material)
return sharpen(ob)
def slab(name, pts, z0, z1, material):
ob = prism(name, pts, z1 - z0, loc=(0, 0, z0), material=material)
return sharpen(ob)
def x_on_bent(y):
"""벤트사이드 바깥선의 y 에서의 x."""
for (x0, y0), (x1, y1) in zip(BENT_DENSE, BENT_DENSE[1:]):
if y0 <= y <= y1 and y1 > y0:
return x0 + (x1 - x0) * (y - y0) / (y1 - y0)
return -HW
def y_on_bent(x):
"""벤트사이드 바깥선이 x 를 지나는 y (꼬리 쪽 단조 구간)."""
best = None
for (x0, y0), (x1, y1) in zip(BENT_DENSE, BENT_DENSE[1:]):
if min(x0, x1) <= x <= max(x0, x1) and x0 != x1:
y = y0 + (y1 - y0) * (x - x0) / (x1 - x0)
best = y if best is None else min(best, y)
return best if best is not None else 2.7
OUT = outline()
INN = offset(OUT, RIM_T)
# ---------------------------------------------------------------- 케이스
rim = ring_wall('Case_Rim', OUT, INN, Z_BOT, Z_TOP, EBONY)
bottom_pts = offset([(-HW, -0.03), (HW, -0.03)] + OUT[2:], 0.008)
case_bottom = slab('Case_Bottom', bottom_pts, Z_BOT - 0.025, Z_BOT + 0.001, EBONY)
soundboard = slab('Soundboard', offset(OUT, RIM_T - 0.004), 0.785, 0.80, SPRUCE)
liner = ring_wall('Rim_Liner', offset(OUT, RIM_T - 0.003), offset(OUT, RIM_T + 0.035), 0.80, 0.83, EBONY)
# 치크 블록 (건반 양옆) — 측면 프로필을 X 방향으로 돌출
cheek_prof = [(-0.03, Z_BOT), (Y_FRONT, Z_BOT), (Y_FRONT, Z_TOP), (0.262, Z_TOP), (0.232, 0.972), (0.205, 0.952),
(0.182, 0.92), (0.162, 0.876), (0.142, 0.836), (0.118, 0.812), (0.085, 0.802), (0.0, 0.80),
(-0.022, 0.796), (-0.03, 0.785)]
cw = HW - CHEEK_X
for side, sx in (('L', -1), ('R', 1)):
ck = extrude_profile(f'Cheek_Block_{side}', cheek_prof, cw, loc=(sx * (CHEEK_X + cw / 2), 0, 0),
material=EBONY, axis='X')
sharpen(ck)
# ---------------------------------------------------------------- 건반부
key_frame = box('Key_Frame', (2 * CHEEK_X - 0.002, 0.215, 0.08), loc=(0, 0.0775, Z_BOT), material=EBONY, base=True)
key_slip = box('Key_Slip', (2 * CHEEK_X - 0.002, 0.022, 0.078), loc=(0, -0.011, Z_BOT + 0.014), material=EBONY, base=True)
felt_strip = box('Key_Felt_Strip', (2 * CHEEK_X - 0.004, 0.012, 0.004), loc=(0, 0.176, 0.722), material=FELT_R, base=True)
NW = 52
kw = 0.0235
kx0 = -NW * kw / 2
wkey = box('White_Key', (kw - 0.0013, 0.17, 0.022), loc=(0, 0.085, 0.70), material=IVORY, base=True)
bevel(wkey, 0.0015, 1)
bkey_proto = box('Black_Key', (0.0118, 0.095, 0.034), loc=(0, 0.1225, 0.701), material=KEYBLK, base=True)
bevel(bkey_proto, 0.002, 1)
names = 'ABCDEFG'
nb = 0
for i in range(NW):
cx = kx0 + (i + 0.5) * kw
k = wkey if i == 0 else duplicate(wkey, name=f'White_Key')
k.location.x = cx
if names[i % 7] in 'ACDFG' and i < NW - 1:
b = bkey_proto if nb == 0 else duplicate(bkey_proto, name='Black_Key')
b.location.x = kx0 + (i + 1) * kw
nb += 1
# 건반 뚜껑(열린 상태로 세워진 폴보드)과 네임보드 블록
fallboard = box('Fallboard', (2 * CHEEK_X - 0.004, 0.03, 0.13), loc=(0, 0.198, 0.724), rot=(math.radians(-8), 0, 0),
material=EBONY, base=True)
bevel(fallboard, 0.006, 2)
nameboard = box('Nameboard_Block', (2 * CHEEK_X - 0.002, 0.11, 0.25), loc=(0, 0.258, Z_BOT), material=EBONY, base=True)
logo = box('Fallboard_Inlay', (0.16, 0.004, 0.012), loc=(0, 0.2015, 0.80), rot=(math.radians(-8), 0, 0), material=GOLD)
# 보면대
desk_angle = math.radians(-14)
desk = box('Music_Desk', (0.86, 0.016, 0.30), loc=(0, 0.262, 0.868), rot=(desk_angle, 0, 0), material=EBONY, base=True)
bevel(desk, 0.004, 2)
desk_ledge = box('Music_Desk_Ledge', (0.86, 0.05, 0.016), loc=(0, 0.245, 0.869), material=EBONY, base=True)
bevel(desk_ledge, 0.003, 1)
desk_lip = box('Music_Desk_Lip', (0.86, 0.008, 0.026), loc=(0, 0.222, 0.869), material=EBONY, base=True)
desk_back = box('Music_Desk_Support', (0.05, 0.12, 0.012), loc=(0, 0.33, 0.866), rot=(math.radians(28), 0, 0), material=EBONY)
# ---------------------------------------------------------------- 철골 프레임(플레이트)
PZ0, PZ1 = 0.83, 0.85
plate = slab('Iron_Plate', offset(OUT, RIM_T + 0.012), PZ0, PZ1, GOLD)
open_pts = offset(outline(0.56), RIM_T + 0.13)
cut = prism('Plate_Opening_Cutter', open_pts, 0.1, loc=(0, 0, PZ0 - 0.03))
boolean(plate, cut)
# 앞쪽 바와 방사형 지지대
struts = [((-0.66, 0.60), (-0.55, 2.45)), ((-0.30, 0.60), (-0.38, 2.50)), ((0.05, 0.60), (-0.08, 2.32)),
((0.38, 0.60), (0.17, 1.80)), ((0.64, 0.60), (0.50, 1.20))]
for i, ((x0, y0), (x1, y1)) in enumerate(struts):
L = math.hypot(x1 - x0, y1 - y0)
ang = math.atan2(-(x1 - x0), y1 - y0)
s = box(f'Plate_Strut_{i}', (0.06, L, 0.022), loc=((x0 + x1) / 2, (y0 + y1) / 2, PZ0 - 0.002),
rot=(0, 0, ang), material=GOLD, base=True)
bevel(s, 0.006, 2)
# 플레이트 장식 구멍(앞바의 둥근 구멍)
for i, x in enumerate([-0.52, -0.20, 0.12, 0.44]):
cylinder(f'Plate_Hole_{i}', r=0.028, h=0.004, loc=(x, 0.545, PZ1 - 0.003), material=FELT_K, seg=24)
# ---------------------------------------------------------------- 줄, 핀, 브리지, 댐퍼
SZ = 0.858
unit = box('String_Unit', (1, 1, 1), material=STEEL)
unit_cu = box('String_Unit_Bass', (1, 1, 1), material=COPPER)
for u_ in (unit, unit_cu):
for v_ in u_.data.vertices:
v_.co.y += 0.5 # 줄 원점을 앞끝(핀 쪽)으로
pin = cylinder('Tuning_Pin', r=0.0035, h=0.03, loc=(0, 0, PZ1 - 0.002), material=STEEL, seg=8)
damper = box('Damper', (0.012, 0.035, 0.03), loc=(0, 0, SZ + 0.0015), material=FELT_K, base=True)
damper_top = box('Damper_Head', (0.010, 0.03, 0.012), loc=(0, 0, SZ + 0.031), material=MAPLE, base=True)
unit.hide_render = unit_cu.hide_render = True
unit.hide_viewport = unit_cu.hide_viewport = True
unit['atelier_cutter'] = unit_cu['atelier_cutter'] = True
Y_PIN = 0.40
first = True
def string(proto, name, p0, p1, z, w):
dx, dy = p1[0] - p0[0], p1[1] - p0[1]
L = math.hypot(dx, dy)
s = duplicate(proto, name=name)
s.hide_render = s.hide_viewport = False
del s['atelier_cutter']
s.location = (p0[0], p0[1], z)
s.rotation_euler = (0, 0, math.atan2(-dx, dy))
s.scale = (w, L, w)
return s
bridge_pts = []
N_TREBLE = 66
tx0, tx1 = -0.40, 0.67
for c in range(N_TREBLE):
t = c / (N_TREBLE - 1)
cx = tx0 + (tx1 - tx0) * t
yend = y_on_bent(cx + 0.06) - 0.16 if cx > -0.30 else 2.30 - (cx + 0.40) * 0.6
yend = min(yend, 2.25)
bridge_pts.append((cx, yend - 0.05))
for k in (-1, 0, 1):
x = cx + k * 0.0042
string(unit, 'String_Steel', (x, Y_PIN + 0.01 * (k % 2)), (x, yend), SZ, 0.0016)
p = duplicate(pin, name='Tuning_Pin', loc=(x, Y_PIN - 0.03 * (k % 2) - 0.015 * (k == 0), PZ1 - 0.002))
if cx < 0.45:
duplicate(damper, name='Damper', loc=(cx, 0.585, SZ + 0.0015))
duplicate(damper_top, name='Damper_Head', loc=(cx, 0.585, SZ + 0.031))
N_BASS = 20
for c in range(N_BASS):
t = c / (N_BASS - 1)
fx = -0.735 + 0.30 * t
rx = -0.62 + 0.52 * t
ry = 2.42 - 0.08 * t
for k in (0, 1):
x = fx + k * 0.006
string(unit_cu, 'String_Bass_Copper', (x, Y_PIN), (rx + k * 0.006, ry), SZ + 0.014, 0.0028)
duplicate(pin, name='Tuning_Pin', loc=(x, Y_PIN - 0.02 * k, PZ1 - 0.002))
# 댐퍼: 줄을 따라 y=0.585 지점
u = (0.585 - Y_PIN) / (ry - Y_PIN)
dxp = fx + 0.003 + (rx - fx) * u
duplicate(damper, name='Damper', loc=(dxp, 0.585, SZ + 0.015))
duplicate(damper_top, name='Damper_Head', loc=(dxp, 0.585, SZ + 0.045))
pin.hide_render = pin.hide_viewport = True
damper.hide_render = damper.hide_viewport = True
damper_top.hide_render = damper_top.hide_viewport = True
for o in (pin, damper, damper_top):
o['atelier_cutter'] = True
# 브리지: 향판 위의 곡선 받침
bp = catmull(bridge_pts[::6] + [bridge_pts[-1]], 6)
for j, z in enumerate((0.81, 0.828, 0.846)):
curve_tube(f'Treble_Bridge_{j}', [(x, y, z) for x, y in bp], radius=0.0115, material=MAPLE)
for j, z in enumerate((0.81, 0.83, 0.85, 0.858)):
curve_tube(f'Bass_Bridge_{j}', [(-0.62 + 0.52 * t, 2.42 - 0.08 * t - 0.08, z) for t in (0, 0.5, 1)],
radius=0.0125, material=MAPLE)
# 아그라프 바 (줄 앞단 받침)
box('Agraffe_Bar', (1.42, 0.02, 0.008), loc=(-0.02, Y_PIN + 0.03, PZ1), material=GOLD, base=True)
# ---------------------------------------------------------------- 뚜껑
LID_ANGLE = math.radians(39)
HX = -HW
lid_pts = [(x - HX, y) for x, y in offset(OUT, -0.004)]
lid = prism('Lid', lid_pts, 0.024, material=EBONY)
sharpen(lid)
bevel(lid, 0.004, 2)
flap = box('Lid_Front_Flap', (W - 0.01, 0.30, 0.02), loc=(W / 2, Y_FRONT + 0.15, 0.024), material=EBONY, base=True)
bevel(flap, 0.004, 2)
lid_pivot = bpy.data.objects.new('Lid_Hinge_Pivot', None)
link(lid_pivot)
lid_pivot.location = (HX, 0, Z_TOP)
lid_pivot.rotation_euler = (0, -LID_ANGLE, 0)
lid.parent = lid_pivot
flap.parent = lid_pivot
for i, y in enumerate((0.55, 1.45, 2.30)):
cylinder(f'Lid_Hinge_{i}', r=0.008, h=0.09, loc=(HX - 0.002, y - 0.045, Z_TOP + 0.002), rot=(-math.pi / 2, 0, 0),
material=BRASS, seg=12)
# 뚜껑 받침대: 벤트사이드 림 위 → 뚜껑 밑면
sb_y = 1.10
sb_x = x_on_bent(sb_y) - RIM_T / 2
d_top = 1.40
top = Vector((HX + d_top * math.cos(LID_ANGLE), sb_y, Z_TOP + d_top * math.sin(LID_ANGLE)))
base_p = Vector((sb_x, sb_y, Z_TOP))
v = top - base_p
stick = cylinder('Lid_Prop_Stick', r=0.013, h=v.length, loc=base_p, material=EBONY, seg=16, r2=0.011)
stick.rotation_euler = v.to_track_quat('Z', 'Y').to_euler()
smooth(stick)
cylinder('Prop_Stick_Cup', r=0.02, h=0.01, loc=base_p, material=BRASS, seg=16)
# ---------------------------------------------------------------- 다리와 캐스터
leg_prof = [(0.0, 0.035), (0.05, 0.035), (0.052, 0.045), (0.050, 0.10), (0.046, 0.11), (0.045, 0.12), (0.052, 0.13),
(0.054, 0.14), (0.058, 0.30), (0.064, 0.46), (0.07, 0.49), (0.078, 0.50), (0.078, 0.53), (0.07, 0.54),
(0.07, 0.56), (0.0, 0.56)]
legs_xy = [(-0.68, 0.14), (0.68, 0.14), (-0.36, 2.38)]
leg_proto = lathe('Leg', leg_prof, material=EBONY, seg=32)
sharpen(leg_proto, 40)
ferrule_proto = lathe('Leg_Ferrule', [(0.0, 0.036), (0.053, 0.036), (0.055, 0.045), (0.053, 0.115), (0.0, 0.115)],
material=BRASS, seg=32)
caster_wheel = cylinder('Caster_Wheel', r=0.034, h=0.022, loc=(0, -0.011, 0.034), rot=(-math.pi / 2, 0, 0),
material=BRASS, seg=24, base=False)
caster_wheel.location = (0, 0, 0.034)
caster_fork = box('Caster_Fork', (0.05, 0.03, 0.02), loc=(0, 0, 0.034), material=BRASS, base=True)
for i, (x, y) in enumerate(legs_xy):
for proto in (leg_proto, ferrule_proto, caster_fork):
o = proto if i == 0 else duplicate(proto)
o.location.x, o.location.y = x, y
cw_ = caster_wheel if i == 0 else duplicate(caster_wheel)
cw_.location = (x, y, 0.034)
blk = box(f'Leg_Block_{i}', (0.16, 0.16, 0.04), loc=(x, y, Z_BOT - 0.064), material=EBONY, base=True)
bevel(blk, 0.01, 2)
# ---------------------------------------------------------------- 리라와 페달
LY = 0.20
pbox = box('Pedal_Box', (0.36, 0.13, 0.07), loc=(0, LY, 0.065), material=EBONY, base=True)
bevel(pbox, 0.01, 2)
for side, sx in (('L', -1), ('R', 1)):
pts = [(sx * 0.13, LY, 0.13), (sx * 0.16, LY, 0.22), (sx * 0.15, LY, 0.33), (sx * 0.10, LY, 0.43),
(sx * 0.085, LY, 0.52), (sx * 0.10, LY, 0.59)]
curve_tube(f'Lyre_Arm_{side}', pts, radius=0.022, material=EBONY, resolution=8)
for i, x in enumerate((-0.035, 0.0, 0.035)):
cylinder(f'Lyre_String_{i}', r=0.004, h=0.40, loc=(x, LY, 0.135), material=BRASS, seg=8)
box('Lyre_Top_Block', (0.30, 0.10, 0.04), loc=(0, LY, Z_BOT - 0.064), material=EBONY, base=True)
box('Lyre_Bottom_Rail', (0.30, 0.05, 0.02), loc=(0, LY, 0.133), material=EBONY, base=True)
for side, sx in (('L', -1), ('R', 1)):
curve_tube(f'Lyre_Brace_{side}', [(sx * 0.15, LY + 0.06, 0.10), (sx * 0.20, 0.62, Z_BOT - 0.03)], radius=0.009,
material=EBONY)
for i, x in enumerate((-0.085, 0.0, 0.085)):
pd = box(f'Pedal_{i}', (0.035, 0.11, 0.012), loc=(x, LY - 0.11, 0.085), material=BRASS, base=True)
bevel(pd, 0.004, 2)
box(f'Pedal_Shank_{i}', (0.016, 0.06, 0.01), loc=(x, LY - 0.06, 0.086), material=BRASS, base=True)

다른 모형