
끌어서 회전, 휠·두 손가락으로 확대
이 모형에 대하여
폭스바겐 타입 1, 흔히 비틀이라 불리는 이 차는 페르디난트 포르셰의 설계를 바탕으로 1938년 처음 만들어졌고, 전후 독일 볼프스부르크 공장에서 대량 생산되었다. 공랭식 수평대향 4기통 엔진을 차체 뒤에 얹고 앞에 짐칸을 둔 구조로, 1972년 포드 모델 T의 누적 생산 기록을 넘어섰다.
1963년형은 작은 직사각형 뒷유리와 칼날형 범퍼, 뒤 엔진 덮개 위의 냉각 루버를 갖춘 시기의 비틀이다. 차체는 앞뒤로 둥글게 내려앉는 하나의 아치를 이루고, 네 개의 펜더는 차체와 따로 만들어 볼트로 붙였으며, 앞뒤 펜더 사이를 고무 발판이 잇는다.
이 모델은 루비 레드 도장에 크롬 범퍼와 화이트월 타이어를 단 수출형 모습을 표현했다. 펜더 위의 원형 전조등과 방향지시등, 후미등, 보닛의 크롬 몰딩, 삼각 환기창, 쌍둥이 배기관까지 비틀을 알아보게 하는 요소를 차례로 얹었다.
만든 방법
차체와 펜더는 길이 방향으로 초타원 단면을 이어 붙인 로프트 메시로, 단면의 높이·폭·지수를 단조 3차 보간(PCHIP)으로 이어 매끄러운 곡면을 얻었다. 같은 초타원 식을 음함수로 써서 표면에 광선을 쏘아 창유리·크롬 테·문틈·몰딩·루버를 표면에서 몇 밀리미터 띄워 정확히 얹었다. 바퀴 자리는 원통 불리언으로 펜더를 파냈고, 타이어·화이트월·휠·허브캡은 회전체이며 네 바퀴는 메시를 공유한다. 범퍼는 수평 경로를 따라 칼날형 단면을 쓸어 만들었다. 실내, 엔진 내부, 서스펜션 세부와 문자 엠블럼은 생략했고 유리는 불투명한 짙은 색으로 단순화했다.
build.py
이 파일 하나를 Blender 에서 실행하면 위의 모형이 만들어집니다. 522줄.
# 폭스바겐 비틀 (1963) — 둥근 등껍질 차체와 따로 붙은 네 개의 펜더, 발판을 가진 클래식 비틀을 bpy 코드로만 만든 모델.# 차체와 펜더는 길이 방향(Y)으로 단면을 이어 붙인 로프트 메시다. 단면은 위·아래 지수가 다른# 초타원(superellipse)이고, 높이·폭·지수를 단조 3차 보간(PCHIP)으로 매끄럽게 이었다.# 같은 초타원 식을 음함수로도 써서, 창유리·크롬 테·문틈·몰딩은 차체 표면에 광선을 쏘아# 정확히 얹었다(표면에서 몇 mm 띄워 깜빡임이 없다). 바퀴 자리는 원통 불리언으로 펜더를 파냈고,# 타이어·화이트월·휠·허브캡은 회전체(lathe)이며 네 바퀴는 메시를 공유하는 링크 복제본이다.# 단위는 미터, 앞이 -Y. 전장 4.07 · 전폭 1.54 · 전고 1.50 · 휠베이스 2.40.from atelier import *SHOT = {"azimuth": -42, "elevation": 12, "lens": 55, "light": "studio", "background": "#36404c"}# ── 재질 ───────────────────────────────────────────────PAINT = mat("Paint_RubyRed", "#931f24", rough=0.28, metal=0.1, coat=1.0)CHROME = mat("Chrome", "#d6d9dd", rough=0.1, metal=1.0)RUBBER = mat("Rubber_Black", "#1f1f21", rough=0.7)GLASS = mat("Glass_Tinted", "#1e2a32", rough=0.10, metal=0.2, coat=0.4)TIRE = mat("Tire_Rubber", "#242426", rough=0.88)WHITEWALL = mat("Tire_Whitewall", "#e2dfd5", rough=0.6)WHEEL = mat("Wheel_PearlWhite", "#d8d4c8", rough=0.35, metal=0.1)UNDER = mat("Underbody_Dark", "#151619", rough=0.9)LENS = mat("Headlamp_Lens", "#dfe6ea", rough=0.05, metal=0.4)RED = mat("Taillamp_Red", "#a3161b", rough=0.2)AMBER = mat("Lamp_Amber", "#d98213", rough=0.2)PLATE = mat("Plate_Cream", "#e6e2cf", rough=0.5)AXLE_F, AXLE_R = -1.25, 1.15TRACK_F, TRACK_R = 0.655, 0.675WHEEL_R = 0.325# ── 보간과 로프트 ────────────────────────────────────────def pchip(xs, ys): """단조 3차 에르미트 보간(넘침 없음).""" n = len(xs) h = [xs[i + 1] - xs[i] for i in range(n - 1)] d = [(ys[i + 1] - ys[i]) / h[i] for i in range(n - 1)] m = [0.0] * n m[0], m[-1] = d[0], d[-1] for i in range(1, n - 1): if d[i - 1] * d[i] > 0: w1, w2 = 2 * h[i] + h[i - 1], h[i] + 2 * h[i - 1] m[i] = (w1 + w2) / (w1 / d[i - 1] + w2 / d[i]) def f(x): if x <= xs[0]: return ys[0] if x >= xs[-1]: return ys[-1]나머지 474줄 펼치기접기
lo, hi = 0, n - 1 while hi - lo > 1: mid = (lo + hi) // 2 if xs[mid] <= x: lo = mid else: hi = mid i, hh = lo, h[lo] t = (x - xs[i]) / hh t2, t3 = t * t, t * t * t return ((2 * t3 - 3 * t2 + 1) * ys[i] + (t3 - 2 * t2 + t) * hh * m[i] + (-2 * t3 + 3 * t2) * ys[i + 1] + (t3 - t2) * hh * m[i + 1]) return fclass Loft: """Y 를 따라 초타원 단면(xc, w, zm, top, bot, nu, nl)을 잇는 매끈한 덩어리.""" def __init__(self, spec, sign=1): self.f = {k: pchip([p[0] for p in v], [p[1] for p in v]) for k, v in spec.items()} self.y0 = max(v[0][0] for v in spec.values()) self.y1 = min(v[-1][0] for v in spec.values()) self.sign = sign def P(self, y): g = self.f return (g['xc'](y) * self.sign, g['w'](y), g['zm'](y), g['top'](y), g['bot'](y), g['nu'](y), g['nl'](y)) def F(self, x, y, z): if y <= self.y0 or y >= self.y1: return 1.0 xc, w, zm, top, bot, nu, nl = self.P(y) dz = z - zm n, hg = (nu, max(top - zm, 1e-4)) if dz >= 0 else (nl, max(zm - bot, 1e-4)) return (abs(x - xc) / w) ** n + (abs(dz) / hg) ** n - 1.0 def ring(self, y, nseg): xc, w, zm, top, bot, nu, nl = self.P(y) out = [] for j in range(nseg): a = 2 * math.pi * j / nseg c, s = math.cos(a), math.sin(a) n, hg = (nu, top - zm) if s >= 0 else (nl, zm - bot) out.append((xc + w * math.copysign(abs(c) ** (2 / n), c), y, zm + hg * math.copysign(abs(s) ** (2 / n), s))) return out def mesh(self, name, ny, nseg, material): bm = bmesh.new() mid, half = (self.y0 + self.y1) / 2, (self.y1 - self.y0) / 2 ys = [mid - half * math.cos(math.pi * i / ny) for i in range(ny + 1)] rings = [[bm.verts.new(p) for p in self.ring(y, nseg)] for y in ys] for a, b in zip(rings, rings[1:]): for j in range(nseg): bm.faces.new((a[j], a[(j + 1) % nseg], b[(j + 1) % nseg], b[j])) for r in (rings[0], rings[-1]): c = bm.verts.new(sum((v.co for v in r), Vector()) / len(r)) for j in range(nseg): bm.faces.new((r[j], r[(j + 1) % nseg], c)) bmesh.ops.recalc_face_normals(bm, faces=bm.faces) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = link(bpy.data.objects.new(name, me)) return smooth(assign(ob, material))def hit(F, p0, d, tmax=1.6): """p0(안쪽)에서 d 방향으로 나가며 F=0 표면을 이분법으로 찾는다.""" lo, hi = 0.0, tmax for _ in range(44): mid = (lo + hi) / 2 if F(p0[0] + d[0] * mid, p0[1] + d[1] * mid, p0[2] + d[2] * mid) < 0: lo = mid else: hi = mid return Vector((p0[0] + d[0] * hi, p0[1] + d[1] * hi, p0[2] + d[2] * hi))def normal_at(F, q, e=1e-4): g = Vector((F(q.x + e, q.y, q.z) - F(q.x - e, q.y, q.z), F(q.x, q.y + e, q.z) - F(q.x, q.y - e, q.z), F(q.x, q.y, q.z + e) - F(q.x, q.y, q.z - e))) return g.normalized()def on_surf(F, mapper, a, b, offset=0.0): p0, d = mapper(a, b) q = hit(F, p0, d) return q + normal_at(F, q) * offsetdef patch(name, F, corners, mapper, material, res=(30, 14), sq=4.0, offset=0.006): """표면 위 둥근 사각 패치(창유리·테). corners=(아래앞, 아래뒤, 위앞, 위뒤) 2D 매개.""" P00, P10, P01, P11 = corners bm = bmesh.new() nu_, nv_ = res grid = [] for j in range(nv_ + 1): row = [] for i in range(nu_ + 1): u, v = -1 + 2 * i / nu_, -1 + 2 * j / nv_ m = max(abs(u), abs(v)) if m > 1e-9: r = (abs(u) ** sq + abs(v) ** sq) ** (1 / sq) u, v = u * m / r, v * m / r s, t = (u + 1) / 2, (v + 1) / 2 a = (1 - s) * (1 - t) * P00[0] + s * (1 - t) * P10[0] + (1 - s) * t * P01[0] + s * t * P11[0] b = (1 - s) * (1 - t) * P00[1] + s * (1 - t) * P10[1] + (1 - s) * t * P01[1] + s * t * P11[1] row.append(bm.verts.new(on_surf(F, mapper, a, b, offset))) grid.append(row) for j in range(nv_): for i in range(nu_): bm.faces.new((grid[j][i], grid[j][i + 1], grid[j + 1][i + 1], grid[j + 1][i])) bm.normal_update() bm.faces.ensure_lookup_table() f0 = bm.faces[len(bm.faces) // 2] if f0.normal.dot(normal_at(F, f0.calc_center_median())) < 0: bmesh.ops.reverse_faces(bm, faces=bm.faces) me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = link(bpy.data.objects.new(name, me)) return smooth(assign(ob, material))def grow(corners, e): """창 모서리 매개를 바깥으로 e 만큼 키운다(테 두르기).""" (a0, b0), (a1, b1), (a2, b2), (a3, b3) = corners s0 = -1 if a0 < a1 else 1 return ((a0 + s0 * e, b0 - e), (a1 - s0 * e, b1 - e), (a2 + s0 * e, b2 + e), (a3 - s0 * e, b3 + e))def tube(name, pts, radius, material, seg=8, closed=False): """점 목록을 따라가는 메시 관(회전 최소 프레임). 몰딩·문틈·와이퍼.""" pts = [Vector(p) for p in pts] n = len(pts) tans = [] for i in range(n): a = pts[(i - 1) % n] if (closed or i > 0) else pts[i] b = pts[(i + 1) % n] if (closed or i < n - 1) else pts[i] tans.append((b - a).normalized()) ref = Vector((0, 0, 1)) if abs(tans[0].z) < 0.9 else Vector((1, 0, 0)) nrm = (ref - tans[0] * ref.dot(tans[0])).normalized() bm = bmesh.new() rings = [] for p, t in zip(pts, tans): nrm = (nrm - t * nrm.dot(t)).normalized() bi = t.cross(nrm) rings.append([bm.verts.new(p + (nrm * math.cos(2 * math.pi * k / seg) + bi * math.sin(2 * math.pi * k / seg)) * radius) for k in range(seg)]) pairs = list(zip(rings, rings[1:])) + ([(rings[-1], rings[0])] if closed else []) for r0, r1 in pairs: for k in range(seg): bm.faces.new((r0[k], r0[(k + 1) % seg], r1[(k + 1) % seg], r1[k])) if not closed: 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(name) bm.to_mesh(me) bm.free() return smooth(assign(link(bpy.data.objects.new(name, me)), material))def grow2(c, e=0.022, eb=0.075): """(y, b) 매개 창의 테: y 로 e, 상대높이로 eb 만큼 키운다.""" (a0, b0), (a1, b1), (a2, b2), (a3, b3) = c return ((a0 - e, b0 - eb), (a1 + e, b1 - eb), (a2 - e, b2 + eb), (a3 + e, b3 + eb))def surf_tube(name, F, mapper, pts2d, radius, material, offset=0.0, closed=False): pts = [on_surf(F, mapper, a, b, offset) for a, b in pts2d] return tube(name, pts, radius, material, closed=closed)def lerp_pts(a, b, n): return [(a[0] + (b[0] - a[0]) * i / n, a[1] + (b[1] - a[1]) * i / n) for i in range(n + 1)]def side(F_side, s): return lambda y, z: ((0.0, y, z), (s, 0.0, 0.0))# ── 1. 차체 본체(등껍질) ───────────────────────────────────BODY_SPEC = { 'xc': [(-2.045, 0.0), (2.045, 0.0)], 'top': [(-2.045, 0.55), (-2.00, 0.72), (-1.93, 0.815), (-1.80, 0.885), (-1.50, 0.965), (-1.20, 1.02), (-1.02, 1.055), (-0.95, 1.12), (-0.85, 1.25), (-0.72, 1.39), (-0.55, 1.47), (-0.25, 1.505), (0.05, 1.50), (0.30, 1.465), (0.55, 1.38), (0.78, 1.25), (0.98, 1.12), (1.20, 1.01), (1.45, 0.90), (1.68, 0.78), (1.85, 0.66), (1.96, 0.55), (2.02, 0.46), (2.045, 0.42)], 'bot': [(-2.045, 0.44), (-1.98, 0.38), (-1.85, 0.32), (-1.50, 0.29), (0.0, 0.28), (1.50, 0.29), (1.85, 0.32), (1.98, 0.37), (2.045, 0.40)], 'zm': [(-2.045, 0.48), (-1.90, 0.60), (-1.50, 0.69), (-1.00, 0.78), (-0.60, 0.84), (0.0, 0.85), (0.60, 0.84), (1.00, 0.78), (1.60, 0.67), (1.90, 0.56), (2.045, 0.41)], 'w': [(-2.045, 0.10), (-2.02, 0.27), (-1.95, 0.37), (-1.80, 0.43), (-1.50, 0.48), (-1.20, 0.545), (-1.00, 0.61), (-0.80, 0.645), (-0.50, 0.66), (0.40, 0.66), (0.80, 0.645), (1.05, 0.61), (1.35, 0.56), (1.65, 0.51), (1.85, 0.46), (1.97, 0.38), (2.03, 0.26), (2.045, 0.10)], 'nu': [(-2.045, 2.0), (-1.20, 2.0), (-0.90, 2.1), (-0.60, 2.25), (0.50, 2.25), (0.90, 2.1), (1.20, 2.05), (2.045, 2.0)], 'nl': [(-2.045, 3.5), (2.045, 3.5)],}BODY = Loft(BODY_SPEC)BF = BODY.Fbody = BODY.mesh("Body_Shell", 170, 112, PAINT)# ── 2. 펜더 네 개(불리언으로 바퀴 자리) ──────────────────────def rows_to_spec(rows, nu=2.3, nl=3.0): keys = ('xc', 'w', 'zm', 'top', 'bot') spec = {k: [(r[0], r[i + 1]) for r in rows] for i, k in enumerate(keys)} spec['nu'] = [(rows[0][0], nu), (rows[-1][0], nu)] spec['nl'] = [(rows[0][0], nl), (rows[-1][0], nl)] return specFRONT_FENDER = [ # y, xc, w, zm, top, bot (-2.000, 0.36, 0.04, 0.50, 0.53, 0.47), (-1.970, 0.42, 0.10, 0.56, 0.66, 0.43), (-1.900, 0.48, 0.14, 0.60, 0.76, 0.40), (-1.750, 0.54, 0.18, 0.62, 0.84, 0.38), (-1.550, 0.580, 0.188, 0.62, 0.875, 0.36), (-1.250, 0.592, 0.182, 0.60, 0.885, 0.33), (-0.980, 0.600, 0.175, 0.54, 0.82, 0.31), (-0.800, 0.605, 0.170, 0.46, 0.66, 0.30), (-0.680, 0.610, 0.165, 0.40, 0.50, 0.30), (-0.600, 0.610, 0.150, 0.37, 0.40, 0.30), (-0.570, 0.610, 0.080, 0.34, 0.35, 0.31),]REAR_FENDER = [ (0.500, 0.620, 0.080, 0.34, 0.35, 0.31), (0.530, 0.620, 0.150, 0.37, 0.41, 0.30), (0.620, 0.620, 0.165, 0.42, 0.53, 0.30), (0.780, 0.615, 0.175, 0.50, 0.72, 0.30), (0.950, 0.610, 0.180, 0.56, 0.84, 0.31), (1.200, 0.600, 0.180, 0.60, 0.91, 0.33), (1.420, 0.590, 0.175, 0.62, 0.90, 0.36), (1.620, 0.570, 0.162, 0.63, 0.86, 0.40), (1.780, 0.530, 0.135, 0.63, 0.79, 0.45), (1.890, 0.470, 0.085, 0.62, 0.70, 0.51), (1.935, 0.410, 0.030, 0.60, 0.62, 0.56),]FENDERS = {}for s, sn in ((1, 'R'), (-1, 'L')): for rows, axle, tag in ((FRONT_FENDER, AXLE_F, 'Front'), (REAR_FENDER, AXLE_R, 'Rear')): lf = Loft(rows_to_spec(rows), sign=s) ob = lf.mesh(f"Fender_{tag}_{sn}", 90, 64, PAINT) cut = cylinder(f"WheelWell_Cutter_{tag}_{sn}", r=0.372, h=0.70, loc=(s * 0.66, axle, 0.33), rot=(0, math.pi / 2, 0), seg=64, base=False, material=UNDER) boolean(ob, cut) ob.modifiers[-1].material_mode = 'TRANSFER' apply_all(ob) FENDERS[(tag, sn)] = lf# ── 3. 발판, 바닥, 차축 ───────────────────────────────────for s, sn in ((1, 'R'), (-1, 'L')): rb = box(f"RunningBoard_{sn}", size=(0.31, 1.19, 0.04), loc=(s * 0.62, -0.045, 0.295), base=True, material=RUBBER) bevel(rb, 0.008, 2) box(f"RunningBoard_Trim_{sn}", size=(0.008, 1.17, 0.018), loc=(s * 0.776, -0.045, 0.306), base=True, material=CHROME)box("Floor_Pan", size=(0.86, 3.30, 0.12), loc=(0, 0, 0.19), base=True, material=UNDER)box("Engine_Transaxle", size=(0.74, 0.70, 0.30), loc=(0, 1.50, 0.22), base=True, material=UNDER)for z in (0.30, 0.40): cylinder("Front_Axle_Tube", r=0.04, h=1.20, loc=(0, AXLE_F, z), rot=(0, math.pi / 2, 0), base=False, material=UNDER, seg=16)cylinder("Rear_Swing_Axle", r=0.04, h=1.26, loc=(0, AXLE_R, WHEEL_R), rot=(0, math.pi / 2, 0), base=False, material=UNDER, seg=16)box("Muffler", size=(0.90, 0.16, 0.14), loc=(0, 1.86, 0.27), base=True, material=UNDER)for s in (1, -1): cylinder("Tailpipe", r=0.021, h=0.24, loc=(s * 0.30, 1.95, 0.31), rot=(math.pi / 2, 0, 0), base=False, material=CHROME, seg=16)# ── 4. 바퀴 ───────────────────────────────────────────────tire = lathe("Tire", [(0.19, -0.066), (0.235, -0.074), (0.28, -0.076), (0.31, -0.068), (0.322, -0.045), (0.325, 0.0), (0.322, 0.045), (0.31, 0.068), (0.28, 0.076), (0.235, 0.074), (0.19, 0.066)], material=TIRE, seg=64)ww = lathe("Whitewall", [(0.232, 0.072), (0.234, 0.0778), (0.282, 0.0778), (0.284, 0.073)], material=WHITEWALL, seg=64)rim = lathe("Rim", [(0.0, -0.05), (0.19, -0.05), (0.19, 0.06), (0.198, 0.066), (0.196, 0.071), (0.184, 0.06), (0.16, 0.042), (0.12, 0.034), (0.0, 0.034)], material=WHEEL, seg=64)cap = lathe("Hubcap", [(0.0, 0.094), (0.03, 0.093), (0.06, 0.087), (0.085, 0.074), (0.10, 0.057), (0.108, 0.042), (0.109, 0.037), (0.0, 0.037)], material=CHROME, seg=64)wheel = join([tire, ww, rim, cap], "Wheel_FR")wheel.location = (TRACK_F, AXLE_F, WHEEL_R)wheel.rotation_euler = (0, math.pi / 2, 0)duplicate(wheel, loc=(-TRACK_F, AXLE_F, WHEEL_R), rot=(0, -math.pi / 2, 0), name="Wheel_FL")duplicate(wheel, loc=(TRACK_R, AXLE_R, WHEEL_R), rot=(0, math.pi / 2, 0), name="Wheel_RR")duplicate(wheel, loc=(-TRACK_R, AXLE_R, WHEEL_R), rot=(0, -math.pi / 2, 0), name="Wheel_RL")# ── 5. 창유리와 크롬 테 ────────────────────────────────────def front_ray(x, z): return (x, -0.30, z), (0.0, -1.0, 0.0)def rear_ray(x, z): return (x, 0.30, z), (0.0, 1.0, 0.0)WINDSHIELD = ((-0.47, 1.07), (0.47, 1.07), (-0.41, 1.37), (0.41, 1.37))REAR_WIN = ((-0.30, 1.18), (0.30, 1.18), (-0.25, 1.37), (0.25, 1.37))patch("Windshield_Frame", BF, grow(WINDSHIELD, 0.022), front_ray, CHROME, res=(36, 14), sq=5, offset=0.004)patch("Windshield_Glass", BF, WINDSHIELD, front_ray, GLASS, res=(36, 14), sq=5, offset=0.008)patch("RearWindow_Frame", BF, grow(REAR_WIN, 0.02), rear_ray, CHROME, res=(30, 12), sq=4, offset=0.004)patch("RearWindow_Glass", BF, REAR_WIN, rear_ray, GLASS, res=(30, 12), sq=4, offset=0.008)# 옆창은 (y, 상대높이 b) 매개: 창 윗변이 지붕선을 따라 휘어진다.SILL = 1.07def win_top(y): return BODY.f['top'](y) - 0.118def side_rel(s): return lambda y, b: ((0.0, y, SILL + b * (win_top(y) - SILL)), (s, 0.0, 0.0))DOOR_WIN = ((-0.80, 0.0), (0.02, 0.0), (-0.585, 1.0), (0.02, 1.0))QUARTER_WIN = ((0.15, 0.0), (0.61, 0.0), (0.15, 1.0), (0.45, 1.0))for s, sn in ((1, 'R'), (-1, 'L')): mp = side(BF, s) mr = side_rel(s) patch(f"DoorWindow_Frame_{sn}", BF, grow2(DOOR_WIN), mr, CHROME, res=(30, 12), sq=6, offset=0.004) patch(f"DoorWindow_Glass_{sn}", BF, DOOR_WIN, mr, GLASS, res=(30, 12), sq=6, offset=0.008) patch(f"QuarterWindow_Frame_{sn}", BF, grow2(QUARTER_WIN), mr, CHROME, res=(26, 12), sq=2.6, offset=0.004) patch(f"QuarterWindow_Glass_{sn}", BF, QUARTER_WIN, mr, GLASS, res=(26, 12), sq=2.6, offset=0.008) # 삼각 환기창 칸막이 surf_tube(f"VentWing_Divider_{sn}", BF, mp, [(y, SILL + b * (win_top(y) - SILL)) for y, b in lerp_pts((-0.645, -0.05), (-0.525, 1.05), 6)], 0.007, CHROME, offset=0.010) # 빗물받이(드립 레일) surf_tube(f"Rain_Gutter_{sn}", BF, mp, [(y, win_top(y) + 0.036) for y, _ in lerp_pts((-0.58, 0), (0.50, 0), 24)], 0.006, CHROME, offset=0.003) # 옆 크롬 몰딩 surf_tube(f"Side_Molding_{sn}", BF, mp, lerp_pts((-0.84, 0.80), (0.74, 0.80), 30), 0.0055, CHROME, offset=0.001) # 문틈 surf_tube(f"Door_Seam_Front_{sn}", BF, mp, lerp_pts((-0.845, 0.36), (-0.835, 1.04), 14), 0.0035, UNDER) surf_tube(f"Door_Seam_Rear_{sn}", BF, mp, lerp_pts((0.085, 0.36), (0.085, 1.04), 14), 0.0035, UNDER) surf_tube(f"Door_Seam_Bottom_{sn}", BF, mp, lerp_pts((-0.845, 0.365), (0.085, 0.365), 20), 0.0035, UNDER) # 문손잡이 q = on_surf(BF, mp, -0.03, 1.00) n = normal_at(BF, q) box(f"Door_Handle_{sn}", size=(0.022, 0.15, 0.024), loc=q + n * 0.008, rot=Vector((1, 0, 0)).rotation_difference(n).to_euler(), material=CHROME) bevel(bpy.data.objects[f"Door_Handle_{sn}"], 0.008, 2)# ── 6. 범퍼(블레이드형) ────────────────────────────────────def sweep(name, path, section, material): """수평 경로를 따라 단면(s=바깥 수평, t=위)을 쓸어 만든다.""" bm = bmesh.new() rings = [] for i, p in enumerate(path): a, b = Vector(path[max(i - 1, 0)]), Vector(path[min(i + 1, len(path) - 1)]) tg = b - a tg.z = 0 tg.normalize() nr = Vector((tg.y, -tg.x, 0)) rings.append([bm.verts.new(Vector(p) + nr * s_ + Vector((0, 0, t_))) for s_, t_ in section]) k = len(section) for r0, r1 in zip(rings, rings[1:]): for j in range(k): bm.faces.new((r0[j], r0[(j + 1) % k], r1[(j + 1) % k], r1[j])) 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(name) bm.to_mesh(me) bm.free() return smooth(assign(link(bpy.data.objects.new(name, me)), material))BLADE = [(-0.006, -0.050), (0.018, -0.052), (0.031, -0.032), (0.035, 0.0), (0.033, 0.030), (0.023, 0.050), (0.008, 0.058), (-0.006, 0.054)]def bumper_path(y0, z, sgn): half = [(x, y0 + 0.05 * (x / 0.66) ** 2) for x in [0.66 * i / 10 for i in range(11)]] half += [(0.705, y0 + 0.066), (0.740, y0 + 0.095), (0.762, y0 + 0.138), (0.772, y0 + 0.185)] full = [(-x, y) for x, y in reversed(half[1:])] + half pts = [(x, y if sgn < 0 else -y, z) for x, y in full] return pts if sgn < 0 else list(reversed(pts))for y0, z, sgn, tag in ((-2.035, 0.43, -1, 'Front'), (-1.995, 0.45, 1, 'Rear')): sweep(f"Bumper_{tag}", bumper_path(y0, z, sgn), BLADE, CHROME) yb = -y0 if sgn > 0 else y0 for s in (1, -1): g = box(f"Bumper_Guard_{tag}", size=(0.045, 0.05, 0.17), loc=(s * 0.36, yb + sgn * (0.035 + 0.006), z + 0.02), material=CHROME) bevel(g, 0.018, 3) box(f"Bumper_Bracket_{tag}", size=(0.045, 0.26, 0.05), loc=(s * 0.42, yb - sgn * 0.13, z), material=UNDER)# ── 7. 전조등·방향지시등·후미등 ────────────────────────────def lamp_rot(n): return Vector((0, 0, 1)).rotation_difference(n).to_euler()bucket = lathe("Headlamp_Bucket", [(0.0, -0.13), (0.104, -0.13), (0.112, -0.02), (0.108, 0.0)], material=PAINT, seg=40)hrim = torus("Headlamp_Rim", R=0.100, r=0.012, seg=40, rseg=10, material=CHROME)hlens = lathe("Headlamp_Lens", [(0.0, 0.032), (0.04, 0.029), (0.07, 0.020), (0.095, 0.006), (0.099, -0.004)], material=LENS, seg=40)headlamp = join([bucket, hrim, hlens], "Headlamp_R")tl_body = sphere("Taillamp_Housing", r=1.0, scale=(0.040, 0.060, 0.018), material=CHROME, seg=24, rings=12)tl_lens = sphere("Taillamp_Lens", r=1.0, scale=(0.034, 0.053, 0.020), loc=(0, 0.004, 0.006), material=RED, seg=24, rings=12)taillamp = join([tl_body, tl_lens], "Taillamp_R")ts_body = sphere("TurnSignal_Body", r=1.0, scale=(0.022, 0.055, 0.022), material=CHROME, seg=16, rings=8)ts_lens = sphere("TurnSignal_Lens", r=1.0, scale=(0.017, 0.022, 0.017), loc=(0, -0.045, 0), material=AMBER, seg=16, rings=8)tsig = join([ts_body, ts_lens], "TurnSignal_R")for s, sn in ((1, 'R'), (-1, 'L')): ff = FENDERS[('Front', sn)].F q = hit(ff, (s * 0.570, -1.55, 0.76), (0, -1, 0)) d = Vector((0, -1, 0.10)).normalized() hl = headlamp if s > 0 else duplicate(headlamp, name="Headlamp_L") hl.location = q + d * 0.015 hl.rotation_euler = lamp_rot(d) q = hit(ff, (s * 0.62, -1.42, 0.5), (0, 0, 1)) ts = tsig if s > 0 else duplicate(tsig, name="TurnSignal_L") ts.location = q + Vector((0, 0, 0.012)) rf = FENDERS[('Rear', sn)].F q = hit(rf, (s * 0.56, 1.55, 0.73), (0, 1, 0)) n = normal_at(rf, q) tl = taillamp if s > 0 else duplicate(taillamp, name="Taillamp_L") tl.location = q + n * 0.004 tl.rotation_euler = Vector((0, 0, 1)).rotation_difference(n).to_euler()# ── 8. 보닛·엔진 덮개 디테일 ─────────────────────────────def up_ray(x, y): return (x, y, 0.45), (0.0, 0.0, 1.0)def loop_pts(c0, c1, hw0, hw1, hl, n=4.0, k=48): """평면 위 둥근 사다리꼴 고리. c0~c1 축 방향, 폭은 hw0→hw1 로 변한다.""" out = [] for i in range(k): a = 2 * math.pi * i / k c, s_ = math.cos(a), math.sin(a) v = math.copysign(abs(s_) ** (2 / n), s_) t = (v + 1) / 2 hw = hw0 + (hw1 - hw0) * t out.append((math.copysign(abs(c) ** (2 / n), c) * hw, c0 + (c1 - c0) * t)) return outsurf_tube("Hood_Seam", BF, up_ray, loop_pts(-1.975, -1.09, 0.24, 0.40, 0, 3.0), 0.0035, UNDER, closed=True)surf_tube("Hood_Trim_Strip", BF, up_ray, lerp_pts((0, -1.93), (0, -1.14), 20), 0.007, CHROME, offset=0.001)q = on_surf(BF, up_ray, 0.0, -1.945)n = normal_at(BF, q)cylinder("Hood_Emblem_VW", r=0.042, h=0.008, loc=q + n * 0.002, rot=lamp_rot(n), material=CHROME, seg=32)box("Hood_Handle", size=(0.10, 0.022, 0.018), loc=on_surf(BF, up_ray, 0.0, -1.885, 0.012), rot=lamp_rot(n), material=CHROME)# 엔진 덮개 테두리, 냉각 루버, 번호판surf_tube("EngineLid_Seam", BF, rear_ray, [(x, z) for x, z in loop_pts(0.50, 1.00, 0.36, 0.30, 0, 3.0)], 0.0035, UNDER, closed=True)for s in (1, -1): for k in range(4): z = 1.035 + k * 0.024 q = on_surf(BF, rear_ray, s * 0.155, z) n = normal_at(BF, q) box("Engine_Louver", size=(0.19, 0.012, 0.009), loc=q, rot=Vector((0, 1, 0)).rotation_difference(n).to_euler(), material=UNDER)q = on_surf(BF, rear_ray, 0.0, 0.60)n = normal_at(BF, q)r = Vector((0, 1, 0)).rotation_difference(n).to_euler()box("License_Plate", size=(0.31, 0.008, 0.155), loc=q + n * 0.012, rot=r, material=PLATE)q = on_surf(BF, rear_ray, 0.0, 0.715)n = normal_at(BF, q)sphere("Plate_Light_Housing", r=1.0, scale=(0.09, 0.035, 0.035), loc=q, rot=Vector((0, 1, 0)).rotation_difference(n).to_euler(), material=CHROME, seg=24, rings=12)# 와이퍼for x0, x1 in ((-0.42, -0.10), (0.06, 0.38)): surf_tube("Wiper", BF, front_ray, lerp_pts((x0, 1.09), (x1, 1.11), 8), 0.006, RUBBER, offset=0.016)






