
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
현대자동차가 1988년에 내놓은 2세대 쏘나타(Y2)는 스텔라를 바탕으로 했던 1세대와 달리 전륜구동 구성으로 새로 개발된 중형 세단이다. 국내 중형차 시장의 대표 차종으로 자리 잡았고, 북미 등 해외 시장에도 수출되었다.
차체는 보닛·캐빈·트렁크가 뚜렷이 나뉜 3박스 형태로, 평평한 보닛과 트렁크, 곧게 선 필러, 넓은 유리 면적이 1980년대 후반 세단의 전형을 보여 준다. 앞면에는 가로로 긴 사각 헤드램프와 가로 살 그릴이, 뒷면에는 차폭을 가로지르는 테일램프가 놓인다.
이 모델은 전장 약 4.68m, 휠베이스 2.65m의 비례를 따라 차체와 캐빈, 6개의 옆창, 크롬 범퍼와 고무 띠, 측면 몰딩, 도어 분할선, 휠캡을 표현했다. 차체색은 당시 흔했던 짙은 와인색 메탈릭으로 칠했다.
만든 방법
하부 차체는 모서리를 둥글린 단면 19개(보간 후 55개)를 길이 방향으로 이어 붙인 로프트 메시이며, 휠하우스는 원통 불리언으로 파냈다. 캐빈은 사다리꼴 절두체에 베벨을 준 단순한 형태이고, 유리는 각 평면 위에 얇은 판을 띄워 올렸다. 범퍼는 평면도 윤곽을 돌출한 뒤 베벨을 적용했고, 바퀴는 회전체 타이어와 휠캡을 합쳐 링크 복제로 네 개를 배치했다. 도어·후드·트렁크 분할선은 차체 표면을 따라가는 가는 관으로 표시했다. 마지막에 바퀴 바깥 오버행만 압축해 범퍼 끝까지 전장을 맞췄으며, 실내와 하부 기계 장치, 곡면의 미세한 굴곡은 생략했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 520줄.
# 현대 쏘나타 2세대 출시형(Y2, 1988) — 각진 3박스 세단을 bpy 코드로만 만든 모델.# 차체 하부는 길이 방향으로 단면(모서리를 둥글린 다각형)을 이어 붙인 로프트 메시이고,# 바퀴 구멍은 원통 불리언으로 파냈다. 캐빈은 사다리꼴 절두체에 베벨을 준 뒤# 앞·뒤·옆 유리를 각 평면 위에 얇은 판으로 올렸다. 사각 헤드램프, 크롬 범퍼,# 가로로 긴 테일램프, 측면 몰딩과 휠캡은 박스·회전체·관(curve) 조합이다.# 바퀴 네 개는 메시를 공유하는 링크 복제본이다. 단위는 미터, 앞이 -Y 방향.from atelier import *SHOT = {"azimuth": -50, "elevation": 13, "lens": 55, "light": "studio", "background": "#3b4552"}# ── 재질 ───────────────────────────────────────────────PAINT = mat("Paint_WineRed", "#6d1a26", rough=0.28, metal=0.35, coat=1.0)CHROME = mat("Chrome", "#d6dadf", rough=0.12, metal=1.0)TRIM = mat("Trim_BlackSatin", "#1d1e21", rough=0.55)GLASS = mat("Glass_Tinted", "#1c2a33", rough=0.04, metal=0.2, coat=1.0)LENS = mat("Headlamp_Lens", "#c9d4da", rough=0.06, metal=0.55)RED = mat("Taillamp_Red", "#9c1016", rough=0.18)AMBER = mat("Lamp_Amber", "#d27a12", rough=0.2)TIRE = mat("Tire_Rubber", "#262628", rough=0.88)WHEEL = mat("Hubcap_Silver", "#b8bcc2", rough=0.3, metal=0.85)DARK = mat("WheelWell_Dark", "#151618", rough=0.9)PLATE = mat("Plate_White", "#e3e1d8", rough=0.5)# ── 주요 치수 (L 4.68 · W 1.75 · H 1.40 · 휠베이스 2.65) ───Y_FRONT, Y_REAR = -2.34, 2.34AXLE_F, AXLE_R = -1.39, 1.26WHEEL_R = 0.31HW = 0.875 # 차체 반폭BELT = 0.905 # 벨트라인 높이ROOF = 1.40def set_sharp(ob, deg=35): for p in ob.data.polygons: p.use_smooth = True ob.data.set_sharp_from_angle(angle=math.radians(deg)) return ob# ── 1. 하부 차체: 단면 로프트 ─────────────────────────────# (y, 윗면 높이 zt, 반폭 hw, 바닥 zb)STATIONS = [ (-2.340, 0.760, 0.800, 0.250), (-2.334, 0.778, 0.830, 0.240), (-2.315, 0.790, 0.852, 0.232), (-2.280, 0.800, 0.866, 0.226), (-2.200, 0.812, 0.874, 0.222), (-2.000, 0.832, 0.875, 0.220),나머지 472줄 펼치기접기
(-1.600, 0.862, 0.875, 0.215), (-1.200, 0.890, 0.875, 0.210), (-0.800, 0.915, 0.875, 0.205), (-0.550, 0.930, 0.875, 0.205), (0.000, 0.935, 0.875, 0.205), (0.800, 0.945, 0.875, 0.205), (1.400, 0.958, 0.875, 0.210), (1.800, 0.968, 0.875, 0.220), (2.150, 0.972, 0.874, 0.235), (2.260, 0.970, 0.866, 0.245), (2.315, 0.962, 0.852, 0.252), (2.334, 0.950, 0.830, 0.258), (2.340, 0.935, 0.800, 0.262),]# 벨트라인을 낮춰 80년대 세단 특유의 넓은 유리 면적을 만든다(앞코는 그대로, 뒤로 갈수록 2.5cm 낮춤)STATIONS = [(y, zt - 0.025 * min(1.0, max(0.0, (y + 2.34) / 1.6)), hw, zb) for y, zt, hw, zb in STATIONS]def refine(st, sub=3): """단면 사이를 선형 보간해 로프트를 촘촘하게.""" out = [] for a, b in zip(st, st[1:]): for i in range(sub): t = i / sub out.append(tuple(a[k] + (b[k] - a[k]) * t for k in range(4))) out.append(st[-1]) return outdef section(zt, hw, zb): """오른쪽 반 단면 (x, z) — 아래 중앙에서 위 중앙까지.""" h = zt - zb return [ (0.0, zb), (hw - 0.10, zb), (hw - 0.035, zb + 0.03), (hw - 0.010, zb + 0.12), (hw, zb + 0.30 * h), (hw, zb + 0.62 * h), # 캐릭터 라인 (hw - 0.006, zb + 0.66 * h), (hw - 0.010, zt - 0.07), (hw - 0.030, zt - 0.018), (hw - 0.075, zt), (hw * 0.5, zt + 0.010), (0.0, zt + 0.014), ]def build_lower_body(): bm = bmesh.new() rings = [] for y, zt, hw, zb in refine(STATIONS): half = section(zt, hw, zb) full = half + [(-x, z) for x, z in reversed(half[1:-1])] rings.append([bm.verts.new((x, y, z)) for x, z in full]) n = len(rings[0]) for a, b in zip(rings, rings[1:]): for i in range(n): bm.faces.new((a[i], a[(i + 1) % n], b[(i + 1) % n], b[i])) bm.faces.new(rings[0]) bm.faces.new(list(reversed(rings[-1]))) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new("Body_Lower") bm.to_mesh(me) bm.free() ob = bpy.data.objects.new("Body_Lower", me) link(ob) assign(ob, PAINT) ob.data.materials.append(DARK) # 휠하우스: 좌우 각각 얕은 원통으로 파낸다 for ax in (AXLE_F, AXLE_R): for s in (1, -1): c = cylinder(f"Cutter_Arch_{'F' if ax < 0 else 'R'}{'R' if s > 0 else 'L'}", r=0.375, h=0.36, loc=(s * (HW - 0.30 + 0.18), ax, WHEEL_R + 0.01), rot=(0, math.pi / 2, 0), base=False, seg=48, material=DARK) m = ob.modifiers.new("Arch", 'BOOLEAN') m.object = c m.solver = 'EXACT' m.material_mode = 'TRANSFER' c.hide_render = True c.hide_viewport = True c['atelier_cutter'] = True apply_all(ob) set_sharp(ob, 40) return obbody = build_lower_body()def body_top(y): """해당 y 에서 차체 윗면 높이(중앙).""" for (y0, z0, *_), (y1, z1, *_) in zip(STATIONS, STATIONS[1:]): if y0 <= y <= y1: t = (y - y0) / (y1 - y0) return z0 + (z1 - z0) * t return STATIONS[-1][1]# ── 2. 캐빈(그린하우스): 사다리꼴 절두체 ──────────────────CAB_Y0, CAB_Y1 = -0.66, 1.56 # 벨트라인에서 앞유리/뒷유리 밑단ROOF_Y0, ROOF_Y1 = 0.06, 1.04 # 지붕 앞/뒤 끝CAB_HW, ROOF_HW = 0.835, 0.670CAB_Z0 = 0.90 # 차체 속에 묻히는 밑면def side_x(z): """캐빈 옆면 평면 위 x (높이 z).""" return CAB_HW - (z - BELT) * (CAB_HW - ROOF_HW) / (ROOF - BELT)def build_cabin(): bm = bmesh.new() zb = CAB_Z0 # 밑면을 BELT 아래로 연장해도 옆면이 같은 평면이 되도록 x 를 맞춘다 xb = side_x(zb) yb0 = CAB_Y0 - (BELT - zb) * (ROOF_Y0 - CAB_Y0) / (ROOF - BELT) yb1 = CAB_Y1 + (BELT - zb) * (CAB_Y1 - ROOF_Y1) / (ROOF - BELT) B = [bm.verts.new(v) for v in ((-xb, yb0, zb), (xb, yb0, zb), (xb, yb1, zb), (-xb, yb1, zb))] T = [bm.verts.new(v) for v in ((-ROOF_HW, ROOF_Y0, ROOF), (ROOF_HW, ROOF_Y0, ROOF), (ROOF_HW, ROOF_Y1, ROOF), (-ROOF_HW, ROOF_Y1, ROOF))] bm.faces.new(list(reversed(B))) bm.faces.new((B[0], B[1], T[1], T[0])) # 앞유리 면 bm.faces.new((B[1], B[2], T[2], T[1])) # 오른쪽 bm.faces.new((B[2], B[3], T[3], T[2])) # 뒷유리 면 bm.faces.new((B[3], B[0], T[0], T[3])) # 왼쪽 bm.faces.new(T) # 지붕 bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new("Cabin") bm.to_mesh(me) bm.free() ob = bpy.data.objects.new("Cabin_Greenhouse", me) link(ob) assign(ob, PAINT) bevel(ob, width=0.035, segments=4, angle=20) apply_all(ob) set_sharp(ob, 50) return obcabin = build_cabin()# ── 3. 바퀴 ─────────────────────────────────────────────def build_wheel(): tw = 0.093 # 타이어 반폭 prof = [(0.180, -tw + 0.012), (0.205, -tw), (0.265, -tw - 0.004), (0.295, -tw + 0.012), (WHEEL_R, -tw + 0.04), (WHEEL_R, tw - 0.04), (0.295, tw - 0.012), (0.265, tw + 0.004), (0.205, tw), (0.180, tw - 0.012)] tire = lathe("Tire", prof, material=TIRE, seg=96) # 휠캡: 바깥(+z) 쪽으로 볼록한 원판 cap_prof = [(0.0, 0.040), (0.05, 0.039), (0.10, 0.034), (0.145, 0.026), (0.170, 0.010), (0.182, 0.0), (0.182, -0.03), (0.0, -0.03)] cap = lathe("Hubcap", cap_prof, material=WHEEL, seg=96) parts = [tire, cap] # 휠캡 구멍(통풍 슬롯) 8개 for i in range(8): a = 2 * math.pi * i / 8 s = box(f"Hubcap_Slot_{i}", (0.045, 0.018, 0.012), loc=(0.115 * math.cos(a), 0.115 * math.sin(a), 0.031), rot=(0, 0, a), material=DARK) parts.append(s) hub = cylinder("Hub_Center", r=0.038, h=0.012, loc=(0, 0, 0.036), material=CHROME, seg=32) parts.append(hub) w = join(parts, "Wheel_FR") for p in w.data.polygons: p.use_smooth = True w.data.set_sharp_from_angle(angle=math.radians(40)) return wwheel = build_wheel()TRACK_X = 0.735wheel.rotation_euler = (0, math.pi / 2, 0)wheel.location = (TRACK_X, AXLE_F, WHEEL_R)duplicate(wheel, loc=(TRACK_X, AXLE_R, WHEEL_R), name="Wheel_RR")duplicate(wheel, loc=(-TRACK_X, AXLE_F, WHEEL_R), rot=(0, -math.pi / 2, 0), name="Wheel_FL")duplicate(wheel, loc=(-TRACK_X, AXLE_R, WHEEL_R), rot=(0, -math.pi / 2, 0), name="Wheel_RL")# ── 4. 유리: 각 캐빈 면 위에 얇은 판 ─────────────────────def slab(name, pts, normal, material, off=0.004, th=0.006): """평면 위 다각형 pts(3D)를 normal 방향으로 off 띄우고 th 두께의 판으로.""" n = Vector(normal).normalized() bm = bmesh.new() lo = [bm.verts.new(Vector(p) + n * off) for p in pts] hi = [bm.verts.new(Vector(p) + n * (off + th)) for p in pts] bm.faces.new(list(reversed(lo))) bm.faces.new(hi) k = len(pts) for i in range(k): bm.faces.new((lo[i], lo[(i + 1) % k], hi[(i + 1) % k], hi[i])) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) return _slab_obj(name, bm, material)def _slab_obj(name, bm, material): 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)def side_pt(s, y, z): return (s * side_x(z), y, z)SIDE_N = (ROOF - BELT, 0, CAB_HW - ROOF_HW) # 오른쪽 옆면 바깥 법선(x,z 성분)def a_line(z): # A 필러 앞 모서리 y return CAB_Y0 + (z - BELT) * (ROOF_Y0 - CAB_Y0) / (ROOF - BELT)def c_line(z): # C 필러 뒤 모서리 y return CAB_Y1 - (z - BELT) * (CAB_Y1 - ROOF_Y1) / (ROOF - BELT)WZ0, WZ1 = 0.955, 1.355 # 옆창 밑/윗단for s, tag in ((1, "R"), (-1, "L")): nrm = (s * SIDE_N[0], 0, SIDE_N[2]) # 검은 창틀 띠(유리 뒤) frame = [(a_line(WZ0 - 0.012) + 0.075, WZ0 - 0.012), (c_line(WZ0 - 0.012) - 0.11, WZ0 - 0.012), (c_line(WZ1 + 0.012) - 0.11, WZ1 + 0.012), (a_line(WZ1 + 0.012) + 0.075, WZ1 + 0.012)] slab(f"Window_Frame_{tag}", [side_pt(s, y, z) for y, z in frame][::s], nrm, TRIM, off=0.001, th=0.003) wins = { "Front": [(a_line(WZ0) + 0.095, WZ0), (0.40, WZ0), (0.40, WZ1), (a_line(WZ1) + 0.095, WZ1)], "Rear": [(0.47, WZ0), (1.00, WZ0), (1.00, WZ1), (0.47, WZ1)], "Quarter": [(1.05, WZ0), (c_line(WZ0) - 0.13, WZ0), (c_line(WZ1) - 0.13, WZ1), (1.05, WZ1)], } for k, poly in wins.items(): pts = [side_pt(s, y, z) for y, z in poly] if poly[2][0] <= poly[3][0] + 1e-3: pts = pts[:2] + [pts[3]] slab(f"Glass_Side_{k}_{tag}", pts[::s], nrm, GLASS, off=0.003, th=0.004)# 앞유리 / 뒷유리def screen(name, yb, yt, sign): zb_, zt_ = BELT + 0.03, ROOF - 0.035 def y_at(z): return yb + (z - BELT) * (yt - yb) / (ROOF - BELT) def hw_at(z): return side_x(z) - 0.075 pts = [(-hw_at(zb_), y_at(zb_), zb_), (hw_at(zb_), y_at(zb_), zb_), (hw_at(zt_), y_at(zt_), zt_), (-hw_at(zt_), y_at(zt_), zt_)] dy, dz = (yt - yb), (ROOF - BELT) nrm = (0, -dz * sign, dy * sign) if sign < 0: nrm = (0, dz, -dy) slab(name, pts, nrm, GLASS, off=0.003, th=0.004)screen("Glass_Windshield", CAB_Y0, ROOF_Y0, 1)screen("Glass_Rear", CAB_Y1, ROOF_Y1, -1)# ── 5. 차체 표면을 따라가는 보조 함수 ─────────────────────def station_at(y): y = max(STATIONS[0][0], min(STATIONS[-1][0], y)) for a, b in zip(STATIONS, STATIONS[1:]): if a[0] <= y <= b[0]: t = (y - a[0]) / (b[0] - a[0]) return [a[i] + (b[i] - a[i]) * t for i in (1, 2, 3)]def surf_x(y, z): """오른쪽 옆면에서 (y, z) 의 x.""" zt, hw, zb = station_at(y) pts = section(zt, hw, zb)[1:10] for (x0, z0), (x1, z1) in zip(pts, pts[1:]): if z0 <= z <= z1: return x0 + (x1 - x0) * (z - z0) / (z1 - z0) return hwdef surf_z(x, y): """윗면에서 (x, y) 의 z.""" zt, hw, zb = station_at(y) ax = abs(x) if ax <= hw * 0.5: return zt + 0.014 - 0.004 * ax / (hw * 0.5) return zt + 0.010 * (hw - 0.075 - ax) / (hw * 0.5 - 0.075)GROOVE = 0.0045def groove(name, pts): return curve_tube(name, pts, radius=GROOVE, material=DARK, resolution=2, bevel_res=1)def side_line(name, yz, s=1): return groove(name, [(s * surf_x(y, z), y, z) for y, z in yz])def top_line(name, xy): return groove(name, [(x, y, surf_z(x, y)) for x, y in xy])def steps(a, b, n): return [a + (b - a) * i / n for i in range(n + 1)]# 후드 / 트렁크 / 도어 분할선top_line("Line_Hood_Rear", [(x, -0.70) for x in steps(-0.80, 0.80, 16)])top_line("Line_Trunk_Front", [(x, 1.62) for x in steps(-0.80, 0.80, 16)])for s, tag in ((1, "R"), (-1, "L")): top_line(f"Line_Hood_Side_{tag}", [(s * 0.80, y) for y in steps(-2.33, -0.70, 20)]) top_line(f"Line_Trunk_Side_{tag}", [(s * 0.80, y) for y in steps(1.62, 2.33, 12)]) side_line(f"Line_Door_Front_{tag}", [(-0.62, z) for z in steps(0.30, 0.90, 10)], s) side_line(f"Line_Door_B_{tag}", [(0.44, z) for z in steps(0.27, 0.90, 10)], s) arc = [(1.30, 0.90), (1.30, 0.74)] for a in steps(80, 165, 8): r = math.radians(a) arc.append((AXLE_R + 0.43 * math.cos(r), WHEEL_R + 0.02 + 0.43 * math.sin(r))) arc.append((0.84, 0.28)) side_line(f"Line_Door_Rear_{tag}", arc, s)# ── 6. 범퍼(크롬 + 고무 띠), 앞 스커트 ─────────────────────def bumper_outline(grow=0.0): outer = [(0.89, -2.05), (0.89, -2.27), (0.85, -2.385), (0.72, -2.425)] inner = [(0.70, -2.33), (0.80, -2.322), (0.845, -2.29), (0.862, -2.25), (0.862, -2.05)] out = [] for x, y in outer: out.append((x + grow * (1 if x > 0.8 else 0.3), y - grow * (1 if y < -2.3 else 0))) right = out + inner[::-1][:0] pts = right + [(-x, y) for x, y in reversed(right)] + [(-x, y) for x, y in inner] + list(reversed(inner)) return ptsdef build_bumper(name): parts = [] bar = prism(name + "_Chrome", bumper_outline(), 0.17, loc=(0, 0, 0.285), material=CHROME) bevel(bar, width=0.018, segments=3, angle=25) apply_all(bar) parts.append(bar) strip = prism(name + "_Rubber", bumper_outline(0.008), 0.035, loc=(0, 0, 0.36), material=TRIM) parts.append(strip) b = join(parts, name) set_sharp(b, 40) return bfront_bumper = build_bumper("Bumper_Front")rear_bumper = duplicate(front_bumper, rot=(0, 0, math.pi), name="Bumper_Rear")valance = box("Front_Valance", (1.56, 0.08, 0.09), loc=(0, -2.30, 0.235), material=TRIM)bevel(valance, 0.01, 2)# ── 7. 앞모습: 사각 헤드램프, 그릴, 엠블럼 ───────────────YF = Y_FRONTfor s, tag in ((1, "R"), (-1, "L")): hx = s * 0.515 bez = box(f"Headlamp_Bezel_{tag}", (0.39, 0.03, 0.155), loc=(hx, YF - 0.008, 0.633), material=CHROME) bevel(bez, 0.006, 2) box(f"Headlamp_Lens_{tag}", (0.37, 0.012, 0.135), loc=(hx, YF - 0.019, 0.633), material=LENS) # 렌즈 안 세로 칸살 for i, dx in enumerate((-0.09, 0.0, 0.09)): box(f"Headlamp_Rib_{tag}{i}", (0.006, 0.006, 0.13), loc=(hx + dx, YF - 0.026, 0.633), material=CHROME) box(f"Corner_Lamp_{tag}", (0.085, 0.03, 0.13), loc=(s * 0.765, YF - 0.0, 0.633), material=AMBER) side_lamp = box(f"Side_Marker_{tag}", (0.02, 0.15, 0.10), loc=(s * (surf_x(-2.17, 0.63) - 0.004), -2.17, 0.633), material=AMBER) bevel(side_lamp, 0.006, 2)grille_bg = box("Grille_Back", (0.60, 0.03, 0.14), loc=(0, YF - 0.006, 0.633), material=DARK)for i, (sz, lc) in enumerate((((0.62, 0.02, 0.012), (0, YF - 0.016, 0.708)), ((0.62, 0.02, 0.012), (0, YF - 0.016, 0.558)), ((0.012, 0.02, 0.16), (0.305, YF - 0.016, 0.633)), ((0.012, 0.02, 0.16), (-0.305, YF - 0.016, 0.633)))): box(f"Grille_Frame_{i}", sz, loc=lc, material=CHROME)for i, z in enumerate(steps(0.585, 0.681, 4)): box(f"Grille_Bar_{i}", (0.60, 0.012, 0.007), loc=(0, YF - 0.018, z), material=CHROME)emb = torus("Emblem_Hyundai_Oval", R=0.052, r=0.005, loc=(0, YF - 0.028, 0.633), rot=(math.pi / 2, 0, 0), material=CHROME, seg=40, rseg=8)emb.scale = (1.0, 0.62, 1.0)box("Emblem_Back", (0.10, 0.008, 0.06), loc=(0, YF - 0.024, 0.633), material=CHROME).scale = (1, 1, 1)for i, dx in enumerate((-0.018, 0.018)): box(f"Emblem_H_{i}", (0.008, 0.006, 0.05), loc=(dx, YF - 0.030, 0.633), rot=(0, math.radians(-20), 0), material=DARK)box("Emblem_H_bar", (0.036, 0.006, 0.007), loc=(0, YF - 0.030, 0.633), material=DARK)# ── 8. 뒷모습: 가로 테일램프, 가니시, 번호판, 배기구 ───────YR = Y_REARfor s, tag in ((1, "R"), (-1, "L")): rim = box(f"Taillamp_Housing_{tag}", (0.42, 0.024, 0.165), loc=(s * 0.60, YR + 0.006, 0.78), material=CHROME) bevel(rim, 0.005, 2) for k, (x0, x1, m) in enumerate(((0.63, 0.80, RED), (0.52, 0.63, AMBER), (0.40, 0.52, RED))): box(f"Taillamp_{tag}{k}", (x1 - x0 - 0.008, 0.012, 0.145), loc=(s * (x0 + x1) / 2, YR + 0.02, 0.78), material=m) box(f"Reverse_Lamp_{tag}", (0.10, 0.013, 0.04), loc=(s * 0.46, YR + 0.021, 0.73), material=LENS) wrap = box(f"Taillamp_Wrap_{tag}", (0.02, 0.13, 0.12), loc=(s * (surf_x(2.20, 0.78) - 0.004), 2.20, 0.78), material=RED) bevel(wrap, 0.006, 2)garn = box("Rear_Garnish", (0.78, 0.016, 0.12), loc=(0, YR + 0.006, 0.78), material=TRIM)box("Rear_Garnish_Trim", (0.78, 0.018, 0.01), loc=(0, YR + 0.008, 0.845), material=CHROME)box("Plate_Recess", (0.40, 0.012, 0.19), loc=(0, YR + 0.004, 0.565), material=TRIM)box("Plate_Rear", (0.335, 0.012, 0.155), loc=(0, YR + 0.012, 0.565), material=PLATE)box("Plate_Front", (0.335, 0.012, 0.155), loc=(0, -2.432, 0.37), material=PLATE)sonata = text3d("Badge_SONATA", "SONATA", size=0.05, depth=0.002, loc=(-0.58, YR + 0.004, 0.655), rot=(math.pi / 2, 0, math.pi), material=CHROME)pipe = cylinder("Exhaust_Pipe", r=0.028, h=0.42, loc=(0.52, 2.20, 0.235), rot=(-math.pi / 2, 0, 0), base=False, material=CHROME, seg=20)# ── 9. 측면: 몰딩, 손잡이, 미러, 와이퍼 ────────────────────for s, tag in ((1, "R"), (-1, "L")): for k, (y0, y1) in enumerate(((-2.26, -1.80), (-0.99, 0.86), (1.66, 2.26))): yc, ln = (y0 + y1) / 2, y1 - y0 x = s * (surf_x(yc, 0.52) + 0.006) m = box(f"Side_Molding_{tag}{k}", (0.022, ln, 0.05), loc=(x, yc, 0.52), material=TRIM) bevel(m, 0.008, 2) box(f"Side_Molding_Chrome_{tag}{k}", (0.024, ln - 0.01, 0.007), loc=(x, yc, 0.528), material=CHROME) for k, y in enumerate((0.22, 1.14)): box(f"Door_Handle_{tag}{k}", (0.016, 0.13, 0.026), loc=(s * (surf_x(y, 0.83) + 0.004), y, 0.83), material=CHROME) # 사이드 미러 mb = box(f"Mirror_Base_{tag}", (0.06, 0.10, 0.05), loc=(s * (side_x(0.97) + 0.02), -0.47, 0.975), material=TRIM) mh = box(f"Mirror_Head_{tag}", (0.13, 0.06, 0.09), loc=(s * (side_x(0.97) + 0.10), -0.46, 1.0), rot=(0, 0, s * math.radians(-6)), material=TRIM) bevel(mh, 0.015, 3) box(f"Mirror_Glass_{tag}", (0.11, 0.004, 0.07), loc=(s * (side_x(0.97) + 0.10), -0.428, 1.0), rot=(0, 0, s * math.radians(-6)), material=LENS)ws_ang = math.atan2(ROOF - BELT, ROOF_Y0 - CAB_Y0)for k, x in enumerate((-0.32, 0.22)): z = 0.975 n = Vector((0, -math.sin(ws_ang), math.cos(ws_ang))) p = Vector((x, a_line(z), z)) + n * 0.014 box(f"Wiper_{k}", (0.50, 0.012, 0.012), loc=p, rot=(ws_ang, 0, math.radians(4)), material=TRIM)for s, tag in ((1, "R"), (-1, "L")): y0, y1 = a_line(BELT) + 0.07, c_line(BELT) - 0.10 box(f"Belt_Molding_{tag}", (0.012, y1 - y0, 0.014), loc=(s * (side_x(BELT + 0.012) + 0.004), (y0 + y1) / 2, BELT + 0.012), material=CHROME) box(f"Drip_Rail_{tag}", (0.010, ROOF_Y1 - ROOF_Y0 - 0.06, 0.012), loc=(s * (ROOF_HW + 0.002), (ROOF_Y0 + ROOF_Y1) / 2, ROOF - 0.022), material=CHROME)box("Cowl_Panel", (1.50, 0.12, 0.012), loc=(0, a_line(BELT) - 0.06, surf_z(0, -0.72) + 0.002), material=TRIM)# ── 10. 마무리: 커브·글자를 메시로 바꾸고, 앞뒤 오버행을 실측 전장에 맞춘다 ──def to_mesh(ob): dg = bpy.context.evaluated_depsgraph_get() me = bpy.data.meshes.new_from_object(ob.evaluated_get(dg)) new = bpy.data.objects.new(ob.name, me) link(new) new.matrix_world = ob.matrix_world.copy() bpy.data.objects.remove(ob, do_unlink=True) return newfor ob in list(bpy.context.scene.objects): if ob.type in ('CURVE', 'FONT'): to_mesh(ob)# 차체는 범퍼 끝까지 4.68m 가 되도록 앞뒤 바퀴 바깥(|y|>1.8)만 압축한다.OH0, OH_K = 1.8, 0.852def fit_y(y): a = abs(y) return y if a <= OH0 else math.copysign(OH0 + (a - OH0) * OH_K, y)for ob in list(bpy.context.scene.objects): if ob.type != 'MESH' or ob.name.startswith("Wheel") or ob.get('atelier_cutter'): continue if ob.data.users > 1: ob.data = ob.data.copy() mw = ob.matrix_world.copy() inv = mw.inverted() for v in ob.data.vertices: w = mw @ v.co w.y = fit_y(w.y) v.co = inv @ w






