본문으로 건너뛰기
3D 아틀리에
제왕나비 3D 모형

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

이 모형에 대하여

제왕나비는 네발나비과 왕나비아과에 속하는 나비로, 날개를 편 폭이 약 9~10cm이다. 주황색 날개를 굵은 검은 시맥이 칸칸이 나누고, 날개 가장자리의 검은 띠에는 흰 점이 두 줄로 박혀 있다. 이 색은 애벌레 시절 먹은 박주가리류 식물의 독성 물질을 몸에 지니고 있음을 포식자에게 알리는 경고색으로 알려져 있다.

북아메리카 동부의 개체군은 가을이면 캐나다와 미국에서 멕시코 중부의 산지 숲까지 수천 km를 날아가 월동하고, 봄에는 여러 세대에 걸쳐 다시 북쪽으로 돌아온다. 곤충 가운데 가장 잘 알려진 장거리 이동 사례이다.

이 모델은 날개를 펴고 앉은 수컷을 실제 크기로 재현했다. 앞날개 끝의 넓은 검은 부분과 주황·흰 반점, 뒷날개 시맥 위의 작은 검은 향기 비늘 반점(수컷의 표지), 흰 점이 찍힌 검은 몸통, 끝이 곤봉처럼 굵어지는 더듬이, 말려 있는 주둥이를 표현했다. 네발나비과의 특징대로 앞다리는 작게 접혀 있고 가운데다리와 뒷다리 네 개로 서 있다.

만든 방법

날개마다 기부에서 가장자리 곡선(Catmull-Rom 스플라인)까지의 방사형 좌표(t, r)를 정의하고, 이 좌표 위에서 시맥 사이의 주황 칸을 다각형으로 계산한 뒤 마이터 오프셋으로 안쪽으로 줄여 검은 시맥 폭을 남겼다. 검은 날개막은 얇은 판이고, 주황 칸·흰 점·반점은 그보다 조금 두꺼운 판으로 막을 관통하게 끼워 앞뒷면 모두에 무늬가 보이며 면이 겹쳐 깜빡이지 않는다. 오목한 칸은 귀 자르기 삼각분할로 메웠고, 아랫면에는 더 옅은 색을 따로 지정했다. 몸통은 타원체와 마디 굴곡을 넣은 회전체, 다리·더듬이·주둥이는 스플라인을 따라가는 관 메시로 만들었다. 날개는 평평한 판으로 단순화했고 비늘의 결이나 털, 날개 아랫면의 흰 시맥 테두리는 생략했다.

build.py

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

# 제왕나비(Monarch butterfly, Danaus plexippus) — 날개를 펴고 앉은 수컷 한 마리, 실제 크기(날개폭 약 10cm).
# 날개는 "날개 기부에서 바깥 가장자리로 뻗는 방사형 좌표(t, r)"로 표현했다.
# t = 가장자리 곡선(Catmull-Rom 스플라인)을 따라간 비율, r = 기부→가장자리 거리 비율.
# 검은 날개막(얇은 판) 위로 시맥(검은 줄) 사이의 주황 칸을 다각형 오프셋으로 깎아 내어 끼우고,
# 가장자리 검은 띠에는 흰 점 두 줄, 앞날개 끝에는 주황·흰 반점을 같은 좌표계로 배치했다.
# 몸통(머리·가슴·배)은 타원체와 회전체, 다리·더듬이·주둥이는 직접 만든 관(tube) 메시로 만들었다.
# 모든 형상은 밀리미터로 계산한 뒤 마지막에 미터 단위(×0.001)로 바꾼다.
from atelier import *
SHOT = {"azimuth": -72, "elevation": 64, "lens": 70, "light": "day", "background": "#24323a"}
MM = 0.001
# ── 재질 ────────────────────────────────────────────────
M_VEIN = mat('WingMembrane_BlackScales', '#1c1714', 0.62)
M_ORANGE = mat('WingScales_Orange', '#d06a08', 0.5)
M_ORANGE_UNDER = mat('WingScales_Orange_Underside', '#d67a2a', 0.6)
M_TAN_UNDER = mat('WingScales_Tan_Underside', '#d6aa62', 0.6)
M_WHITE = mat('Scales_WhiteSpots', '#ece4d2', 0.55)
M_APEX = mat('Scales_ApexSpots', '#efa24a', 0.55)
M_BODY = mat('Body_BlackCuticle', '#171311', 0.45)
M_EYE = mat('CompoundEye', '#2a2018', 0.25)
M_PROB = mat('Proboscis', '#3a2c20', 0.5)
# ── 2D 도우미 ───────────────────────────────────────────
def catmull(ctrl, per=16):
pts = [ctrl[0]] + list(ctrl) + [ctrl[-1]]
out = []
for i in range(1, len(pts) - 2):
p0, p1, p2, p3 = pts[i - 1], pts[i], pts[i + 1], pts[i + 2]
for k in range(per):
s = k / per
s2, s3 = s * s, s * s * s
out.append(tuple(0.5 * (2 * p1[j] + (-p0[j] + p2[j]) * s + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * s2
+ (-p0[j] + 3 * p1[j] - 3 * p2[j] + p3[j]) * s3) for j in (0, 1)))
out.append(tuple(ctrl[-1]))
return out
def resample(poly, n):
d = [0.0]
for a, b in zip(poly, poly[1:]):
d.append(d[-1] + math.dist(a, b))
total = d[-1]
out, j = [], 0
for i in range(n + 1):
s = total * i / n
나머지 361줄 펼치기
while j < len(d) - 2 and d[j + 1] < s:
j += 1
f = (s - d[j]) / max(d[j + 1] - d[j], 1e-9)
out.append((poly[j][0] + (poly[j + 1][0] - poly[j][0]) * f, poly[j][1] + (poly[j + 1][1] - poly[j][1]) * f))
return out
def signed_area(p):
return 0.5 * sum(p[i][0] * p[(i + 1) % len(p)][1] - p[(i + 1) % len(p)][0] * p[i][1] for i in range(len(p)))
def inset(poly, w):
"""반시계 다각형을 안쪽으로 w 만큼 오프셋(마이터)."""
n = len(poly)
out = []
for i in range(n):
a, b, c = poly[i - 1], poly[i], poly[(i + 1) % n]
e1 = (b[0] - a[0], b[1] - a[1])
e2 = (c[0] - b[0], c[1] - b[1])
l1, l2 = math.hypot(*e1) or 1e-9, math.hypot(*e2) or 1e-9
n1 = (-e1[1] / l1, e1[0] / l1)
n2 = (-e2[1] / l2, e2[0] / l2)
cc = max(n1[0] * n2[0] + n1[1] * n2[1], -0.6)
k = w / (1 + cc)
out.append((b[0] + (n1[0] + n2[0]) * k, b[1] + (n1[1] + n2[1]) * k))
return out
def ellipse(c, ax, ay, ang, seg=12):
ca, sa = math.cos(ang), math.sin(ang)
return [(c[0] + ax * math.cos(2 * math.pi * i / seg) * ca - ay * math.sin(2 * math.pi * i / seg) * sa,
c[1] + ax * math.cos(2 * math.pi * i / seg) * sa + ay * math.sin(2 * math.pi * i / seg) * ca) for i in range(seg)]
class WingShape:
"""기부(0,0)에서 본 날개 가장자리 곡선 M(t). u = 바깥쪽, v = 앞쪽(머리 방향)."""
def __init__(self, ctrl, rot=0.0, n=480):
a = math.radians(rot) # 날개를 앞(+)·뒤(-)로 젖힌 각
ctrl = [(x * math.cos(a) - y * math.sin(a), x * math.sin(a) + y * math.cos(a)) for x, y in ctrl]
self.n = n
self.m = resample(catmull(ctrl), n)
def scallop(self, veins, depth=0.012, hw=0.022):
"""시맥 끝마다 가장자리가 살짝 오목하게 들어가는 물결 모양."""
self.veins, self.depth, self.hw = veins, depth, hw
def M(self, t):
x = min(max(t, 0.0), 1.0) * self.n
i = min(int(x), self.n - 1)
f = x - i
a, b = self.m[i], self.m[i + 1]
k = 1.0
for tv in getattr(self, 'veins', ()):
k -= self.depth * bump(t, tv, self.hw)
return ((a[0] + (b[0] - a[0]) * f) * k, (a[1] + (b[1] - a[1]) * f) * k)
def P(self, t, r):
m = self.M(t)
return (m[0] * r, m[1] * r)
def R(self, t):
return math.hypot(*self.M(t))
def tangent(self, t):
a, b = self.M(t - 0.004), self.M(t + 0.004)
return math.atan2(b[1] - a[1], b[0] - a[0])
# ── 메시 조립 ───────────────────────────────────────────
class MeshBuilder:
def __init__(self):
self.bm = bmesh.new()
def slab(self, poly, h0, h1, frame, top_mi, bot_mi, side_mi):
"""2D 다각형을 날개 좌표계에서 두께 방향(h0~h1)으로 세운 판. 윗면·아랫면은 다각형 면."""
if signed_area(poly) < 0:
poly = list(reversed(poly))
V = self.bm.verts.new
top = [V(frame(p[0], p[1], h1)) for p in poly]
bot = [V(frame(p[0], p[1], h0)) for p in poly]
n = len(poly)
caps = [self.bm.faces.new(top), self.bm.faces.new(list(reversed(bot)))]
caps[0].material_index = top_mi
caps[1].material_index = bot_mi
for i in range(n):
j = (i + 1) % n
f = self.bm.faces.new((bot[i], bot[j], top[j], top[i]))
f.material_index = side_mi
# 오목한 칸도 겹치지 않게 귀 자르기(ear clip)로 삼각분할
bmesh.ops.triangulate(self.bm, faces=caps, quad_method='BEAUTY', ngon_method='EAR_CLIP')
def finish(self, name, mats, smooth_faces=False):
bmesh.ops.recalc_face_normals(self.bm, faces=self.bm.faces)
me = bpy.data.meshes.new(name)
self.bm.to_mesh(me)
self.bm.free()
ob = bpy.data.objects.new(name, me)
link(ob)
for m in mats:
me.materials.append(m)
if smooth_faces:
smooth(ob)
return ob
def wing_frame(side, base, dihedral, sweep):
"""날개 2D(u, v, 두께 h) → 몸 좌표(mm). side=+1 오른쪽, -1 왼쪽. 머리는 -Y."""
d, s = math.radians(dihedral), math.radians(sweep)
eu = Vector((side * math.cos(d) * math.cos(s), -math.cos(d) * math.sin(s), math.sin(d)))
ev = Vector((side * math.sin(s), -math.cos(s), 0.0))
ev = (ev - eu * ev.dot(eu)).normalized()
en = eu.cross(ev).normalized()
if en.z < 0:
en = -en
b = Vector(base)
return lambda u, v, h: tuple(b + eu * u + ev * v + en * h)
def bump(t, c, hw):
x = (t - c) / hw
return 0.5 * (1 + math.cos(math.pi * x)) if abs(x) < 1 else 0.0
def build_wing(name, shape, veins, discal, border_fn, vein_w, frame, under_mat, spot_rows, apex=None, hide_front=0.0, scent=None):
"""
veins: 시맥이 가장자리에 닿는 t 값 목록(0, …, 1)
discal: (t0, t1, k) — 중실(discal cell) 범위와 바깥 경계 r
border_fn(t): 가장자리 검은 띠 폭(mm)
spot_rows: 흰 점 열 [(가장자리로부터 깊이 mm, 칸당 개수, 반지름)]
"""
mb = MeshBuilder()
# 0 검은 막, 1 주황(윗면), 2 아랫면 색, 3 흰 점, 4 앞날개 끝 주황 반점
HM, HC, HS = 0.10, 0.24, 0.26
# 날개막: 기부 둘레 부채꼴 + 가장자리 곡선
outline = [(0.0, 0.0)] + [shape.M(i / 240) for i in range(241)]
outline = [(p[0] * 1.012, p[1] * 1.012) for p in outline] # 가장자리 테가 칸보다 살짝 크다
mb.slab(outline, -HM, HM, frame, 0, 0, 0)
t0d, t1d, kd = discal
r_in = 0.13
def cell(ta, tb, ra, outer=True):
n = max(4, int((tb - ta) * 160))
pts = []
for i in range(n + 1):
t = ta + (tb - ta) * i / n
pts.append(shape.P(t, ra))
for i in range(n, -1, -1):
t = ta + (tb - ta) * i / n
rb = 1 - border_fn(t) / shape.R(t) if outer else kd
pts.append(shape.P(t, rb))
if signed_area(pts) < 0:
pts.reverse()
poly = inset(pts, vein_w)
if signed_area(poly) < 1.0:
return
mb.slab(poly, -HC, HC, frame, 1, 2, 1)
for ta, tb in zip(veins, veins[1:]):
if ta < hide_front:
continue
inside = t0d - 1e-6 <= ta and tb <= t1d + 1e-6
cell(ta, tb, kd if inside else r_in + 0.05)
cell(t0d, t1d, r_in, outer=False) # 중실
# 가장자리 흰 점
for ta, tb in zip(veins, veins[1:]):
if ta < hide_front:
continue
for depth, count, rad in spot_rows:
for k in range(count):
t = ta + (tb - ta) * (k + 1) / (count + 1)
if border_fn(t) < depth + rad + 0.3:
continue
r = 1 - depth / shape.R(t)
c = shape.P(t, r)
mb.slab(ellipse(c, rad * 1.25, rad, shape.tangent(t), 10), -HS, HS, frame, 3, 3, 3)
# 앞날개 끝 반점(주황 한 줄 + 흰 한 줄)
if apex:
for ta, tb in zip(veins, veins[1:]):
tm = (ta + tb) / 2
if not (apex[0] <= tm <= apex[1]):
continue
bw = border_fn(tm)
for depth_f, a, b, mi in ((0.78, 1.9, 1.1, 4), (0.47, 1.0, 0.75, 3)):
c = shape.P(tm, 1 - bw * depth_f / shape.R(tm))
mb.slab(ellipse(c, a, b, shape.tangent(tm), 12), -HS, HS, frame, mi, mi, mi)
# 수컷 뒷날개 시맥 위의 검은 향기 비늘 반점
if scent:
ts, rs = scent
c = shape.P(ts, rs)
mb.slab(ellipse(c, 2.3, 1.35, math.atan2(*reversed(shape.M(ts))), 14), -HS - 0.02, HS + 0.02, frame, 0, 0, 0)
return mb.finish(name, [M_VEIN, M_ORANGE, under_mat, M_WHITE, M_APEX])
# ── 날개 모양(mm, 기부 기준) ───────────────────────────────
FORE = WingShape([(1.2, 1.6), (6, 3.8), (14, 6.0), (25, 8.0), (36, 9.4), (46, 9.4), (52, 7.6), (54.6, 4.0),
(53.0, -1.2), (48.2, -7.6), (43.0, -13.6), (37.6, -19.4), (32.0, -23.6), (29.0, -24.4),
(22, -19.2), (14, -12.6), (7, -6.4), (2.0, -1.8)], rot=-3)
HIND = WingShape([(1.0, 1.8), (8, 4.2), (17, 4.0), (25, 1.2), (31.5, -4.5), (35, -11.5), (36, -19), (34.5, -26.5),
(30.5, -33), (24.5, -37.8), (17.5, -40), (11, -38.5), (6, -33), (2.8, -24), (1.0, -13), (0.4, -5)], rot=6)
FORE_VEINS = [0.0, 0.30, 0.355, 0.41, 0.465, 0.52, 0.575, 0.63, 0.69, 1.0]
HIND_VEINS = [0.0, 0.17, 0.26, 0.335, 0.41, 0.485, 0.56, 0.635, 0.715, 0.80, 1.0]
FORE.scallop(FORE_VEINS[2:-1])
HIND.scallop(HIND_VEINS[2:-1], depth=0.016)
def fore_border(t):
b = 3.6 + 8.5 * bump(t, 0.375, 0.12) # 앞날개 끝은 넓게 검다
if t < 0.30:
b = 2.2 + 1.2 * bump(t, 0.30, 0.06) # 앞 가장자리(전연) 검은 띠
if t > 0.69:
b = 1.6 + 1.8 * bump(t, 0.69, 0.06) # 뒤 가장자리
return b
def hind_border(t):
if t < 0.17:
return 1.6
if t > 0.80:
return 1.5 + 1.9 * bump(t, 0.80, 0.07)
return 3.6
wings = []
for side, tag in ((1, 'R'), (-1, 'L')):
ff = wing_frame(side, (side * 1.3, -2.6, 1.7), 16, 0)
hf = wing_frame(side, (side * 1.3, 0.1, 0.75), 16, 0)
wings.append(build_wing(f'Forewing_{tag}', FORE, FORE_VEINS, (0.355, 0.63, 0.50), fore_border, 0.72, ff,
M_ORANGE_UNDER, [(1.05, 2, 0.42), (2.45, 1, 0.5)], apex=(0.30, 0.47)))
wings.append(build_wing(f'Hindwing_{tag}', HIND, HIND_VEINS, (0.26, 0.56, 0.44), hind_border, 0.85, hf,
M_TAN_UNDER, [(1.05, 2, 0.4), (2.5, 2, 0.42)], scent=(HIND_VEINS[6], 0.6)))
# ── 관(tube) 메시: 다리·더듬이·주둥이 ───────────────────────
def tube(name, pts, radii, material, seg=10, smooth_path=True):
if smooth_path:
pts = catmull3(pts)
n = len(pts)
if not isinstance(radii, (list, tuple)):
radii = [radii] * n
elif len(radii) != n:
radii = [radii[0] + (radii[-1] - radii[0]) * i / (n - 1) for i in range(n)]
bm = bmesh.new()
rings = []
prev_x = None
for i in range(n):
p = Vector(pts[i])
d = (Vector(pts[min(i + 1, n - 1)]) - Vector(pts[max(i - 1, 0)])).normalized()
if prev_x is None:
ref = Vector((0, 0, 1)) if abs(d.z) < 0.9 else Vector((1, 0, 0))
x = d.cross(ref).normalized()
else:
x = (prev_x - d * prev_x.dot(d)).normalized()
y = d.cross(x)
prev_x = x
rings.append([bm.verts.new(p + (x * math.cos(2 * math.pi * k / seg) + y * math.sin(2 * math.pi * k / seg)) * radii[i])
for k in range(seg)])
for i in range(n - 1):
for k in range(seg):
bm.faces.new((rings[i][k], rings[i][(k + 1) % seg], rings[i + 1][(k + 1) % seg], rings[i + 1][k]))
bm.faces.new(list(reversed(rings[0])))
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)
def catmull3(ctrl, per=8):
pts = [ctrl[0]] + list(ctrl) + [ctrl[-1]]
out = []
for i in range(1, len(pts) - 2):
p0, p1, p2, p3 = pts[i - 1], pts[i], pts[i + 1], pts[i + 2]
for k in range(per):
s = k / per
out.append(tuple(0.5 * (2 * p1[j] + (-p0[j] + p2[j]) * s + (2 * p0[j] - 5 * p1[j] + 4 * p2[j] - p3[j]) * s * s
+ (-p0[j] + 3 * p1[j] - 3 * p2[j] + p3[j]) * s ** 3) for j in range(3)))
out.append(tuple(ctrl[-1]))
return out
# ── 몸통 ───────────────────────────────────────────────
thorax = sphere('Thorax', 1.0, loc=(0, -1.0, 0.2), material=M_BODY, seg=40, rings=24, scale=(2.55, 4.4, 2.5))
head = sphere('Head', 1.0, loc=(0, -6.6, 0.3), material=M_BODY, seg=32, rings=20, scale=(2.0, 1.8, 1.9))
eyes = [sphere(f'CompoundEye_{t}', 1.25, loc=(s * 1.35, -7.3, 0.55), material=M_EYE, seg=24, rings=14)
for s, t in ((1, 'R'), (-1, 'L'))]
# 배: 마디가 드러나는 회전체, 꼬리 쪽이 약간 아래로 처진다
prof = []
L_AB = 19.0
for i in range(61):
z = L_AB * i / 60
x = z / L_AB
r = 2.05 * math.sin(math.pi * min(1.0, 0.12 + 0.88 * x)) ** 0.55 * (1 - 0.25 * x)
r *= 1 - 0.07 * abs(math.sin(math.pi * z / 2.35))
prof.append((max(r, 0.0), z))
prof[0] = (0.0, 0.0)
prof[-1] = (0.0, L_AB)
abdomen = lathe('Abdomen', prof, loc=(0, 2.2, -0.2), rot=(math.radians(-98), 0, 0), material=M_BODY, seg=36)
# 가슴·머리의 흰 점(제왕나비 몸의 특징)
dot = sphere('BodyDot', 0.36, material=M_WHITE, seg=12, rings=8)
dot_pts = []
for sx in (1, -1):
for (ex, ey, ez) in ((0.55, -0.55, 0.62), (0.75, 0.15, 0.55), (0.85, -0.25, -0.25), (0.5, 0.6, 0.6)):
n = Vector((sx * ex, ey, ez)).normalized()
dot_pts.append((sx * 2.55 * n.x, -1.0 + 4.4 * n.y, 0.2 + 2.5 * n.z))
for (ex, ey, ez) in ((0.35, -0.55, 0.76), (0.82, -0.2, -0.45)):
n = Vector((sx * ex, ey, ez)).normalized()
dot_pts.append((sx * 2.0 * n.x, -6.6 + 1.8 * n.y, 0.3 + 1.9 * n.z))
body_dots = [dot] + [duplicate(dot) for _ in dot_pts[1:]]
for ob, p in zip(body_dots, dot_pts):
ob.location = p
ob.name = 'BodyWhiteSpot'
# 더듬이: 길고 끝이 곤봉 모양
antennae = []
for s, t in ((1, 'R'), (-1, 'L')):
pts = [(s * 0.6, -7.6, 1.9), (s * 1.4, -10.5, 3.6), (s * 2.6, -14.5, 5.2), (s * 3.9, -18.5, 6.2), (s * 5.0, -21.5, 6.6)]
antennae.append(tube(f'Antenna_{t}', pts, [0.22, 0.19], M_BODY, seg=8))
antennae.append(sphere(f'AntennaClub_{t}', 0.5, loc=(s * 5.1, -21.9, 6.6), material=M_BODY, seg=12, rings=8,
scale=(1, 2.4, 1), rot=(0, 0, math.radians(-s * 14))))
# 말린 주둥이(proboscis)
coil = []
for i in range(70):
a = 2.6 * math.pi * i / 69
rr = 1.05 * (1 - 0.6 * i / 69)
coil.append((0.0, -7.9 + 0.4 - rr * math.sin(a) * 0.9 - 0.4, -1.6 - rr * (1 - math.cos(a)) * 0.9 + 0.2))
proboscis = tube('Proboscis', coil, [0.2, 0.12], M_PROB, seg=8, smooth_path=False)
palps = [tube(f'LabialPalp_{t}', [(s * 0.5, -7.6, -0.9), (s * 0.6, -8.6, -0.6), (s * 0.55, -9.4, 0.2)], [0.35, 0.2],
M_BODY, seg=8) for s, t in ((1, 'R'), (-1, 'L'))]
# 다리: 앞다리는 작게 접혀 있고, 가운데·뒷다리 네 개로 선다(제왕나비과의 특징)
GROUND = -8.0
legs = []
for s, t in ((1, 'R'), (-1, 'L')):
legs.append(tube(f'Foreleg_Folded_{t}', [(s * 0.9, -3.6, -1.6), (s * 1.6, -5.2, -2.6), (s * 1.3, -6.6, -2.0)],
[0.32, 0.2], M_BODY, seg=8))
legs.append(tube(f'Midleg_{t}', [(s * 0.9, -1.6, -1.8), (s * 3.8, -2.6, -2.2), (s * 6.4, -3.6, -0.6), (s * 8.0, -5.5, -4.5),
(s * 9.0, -7.0, GROUND + 0.12)], [0.36, 0.13], M_BODY, seg=8))
legs.append(tube(f'Hindleg_{t}', [(s * 0.9, 0.6, -1.7), (s * 3.8, 1.6, -2.0), (s * 6.2, 3.4, -0.4), (s * 7.6, 6.0, -4.4),
(s * 8.4, 8.0, GROUND + 0.12)], [0.36, 0.13], M_BODY, seg=8))
# ── 미터 단위로 변환 ─────────────────────────────────────
done = set()
for ob in bpy.context.scene.objects:
if ob.type == 'MESH':
if ob.data.name not in done:
ob.data.transform(Matrix.Scale(MM, 4))
done.add(ob.data.name)
ob.location = ob.location * MM

다른 모형