반쪽만 만들면 나머지 반쪽을 거울처럼 뒤집어 붙여 주는 모디파이어. 좌우 대칭 모델의 기본 출발점이다.
다른 이름: Mirrormodifiers.new(type='MIRROR')
···
bpy — 이 항목
# 왼쪽 반만 만들고 미러 모디파이어로 오른쪽을 채운다.
import bmesh
reset('WORKBENCH')
camera((0, -8, 4.5), target=(0, 0, 1.0))
floor()
bpy.ops.mesh.primitive_cube_add(size=1.2, location=(0, 0, 1.0))
half = bpy.context.object
half.name = 'Half'
bm = bmesh.new()
bm.from_mesh(half.data)
bmesh.ops.subdivide_edges(bm, edges=bm.edges[:], cuts=3, use_grid_fill=True)
# x > 0 쪽 꼭짓점을 지워 왼쪽 절반만 남긴다(가운데 x = 0 줄은 남는다)
bmesh.ops.delete(bm, geom=[v for v in bm.verts if v.co.x > 0.001], context='VERTS')
for v in bm.verts: # 왼쪽 위를 날개처럼 끌어낸다
if v.co.x < -0.5 and v.co.z > 0:
v.co.x -= 0.7
v.co.z += 0.35
bm.to_mesh(half.data)
bm.free()
mirror = half.modifiers.new('Mirror', 'MIRROR')
mirror.use_axis = (True, False, False) # 오브젝트 원점의 X 축을 기준으로 뒤집어 붙인다
mirror.use_clip = True # 편집할 때 가운데 꼭짓점이 반대편으로 넘어가지 않게
mirror.use_mirror_merge = True # 가운데 겹친 꼭짓점을 하나로 합친다
subsurf = half.modifiers.new('Subdivision', 'SUBSURF')
subsurf.levels = subsurf.render_levels = 2
frame(half, margin=1.6) # 미러까지 켠 완성 모양에 맞춰 카메라를 둔다
for mod in half.modifiers: # 렌더에서만 잠시 끈다 — 모디파이어는 그대로 남아 있다
mod.show_render = False
paint(half, material('Clay', CLAY))
shoot('off')
for mod in half.modifiers:
mod.show_render = True
paint(half, material('Lime', LIME))
shoot('on')
전체 스크립트 — Blender 5.1.1 에서 그대로 실행
# mirror-modifier — Blender 5.1.1 에서 그대로 돌아가는 스크립트
# 실행: blender -b --factory-startup --python mirror-modifier.py -- <출력 폴더>
SLUG = 'mirror-modifier'
# ── 무대: 모든 항목이 같이 쓰는 준비 코드 (renders/_stage.py) ──────────────────
# 빈 장면 → 렌더 엔진·크기 → 카메라·바닥·조명·재질 도우미 → shoot() 으로 WebP 저장.
import bpy, math, os, sys
from mathutils import Vector
# 출력 폴더: blender -b --factory-startup --python 이파일.py -- <출력 폴더>
OUT = sys.argv[sys.argv.index('--') + 1] if '--' in sys.argv[:-1] else os.getcwd()
os.makedirs(OUT, exist_ok=True)
# 색은 화면에서 고르는 sRGB 0~1 값으로 적는다. 블렌더 내부는 선형(linear)이라 srgb() 로 바꿔 넣는다.
BACKDROP = (0.24, 0.25, 0.27)
CLAY = (0.80, 0.77, 0.72)
LIME = (0.64, 0.90, 0.21)
SKY = (0.22, 0.65, 0.95)
CORAL = (0.96, 0.42, 0.45)
INK = (0.08, 0.09, 0.10)
ENGINES = {'WORKBENCH': 'BLENDER_WORKBENCH', 'EEVEE': 'BLENDER_EEVEE', 'CYCLES': 'CYCLES'}
def srgb(c, alpha=1.0):
lin = lambda v: v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
return (lin(c[0]), lin(c[1]), lin(c[2]), alpha)
def reset(engine='WORKBENCH', samples=16, size=(960, 600)):
"""빈 장면에서 시작한다. engine: WORKBENCH(모양 확인) · EEVEE(실시간) · CYCLES(경로 추적)."""
bpy.ops.wm.read_factory_settings(use_empty=True)
sc = bpy.context.scene
sc.render.engine = ENGINES[engine]
sc.render.resolution_x, sc.render.resolution_y = size
sc.render.resolution_percentage = 100
sc.render.image_settings.file_format = 'WEBP'
sc.render.image_settings.quality = 82
sc.world = bpy.data.worlds.new('World')
world_color(BACKDROP)
if engine == 'WORKBENCH':
sh = sc.display.shading
sh.light, sh.color_type = 'STUDIO', 'MATERIAL'
sh.show_shadows, sh.shadow_intensity = True, 0.35
sh.show_cavity = True
sc.display.render_aa = '16'
sc.view_settings.view_transform = 'Standard' # 재질 색을 그대로 보여 준다
elif engine == 'EEVEE':
sc.eevee.taa_render_samples = samples
else:
sc.cycles.samples = samples
sc.cycles.use_denoising = True
return sc
def world_color(color, strength=1.0):
"""배경색. Workbench 는 world.color, EEVEE·Cycles 는 Background 노드를 쓴다."""
w = bpy.context.scene.world
w.color = srgb(color)[:3]
bg = w.node_tree.nodes.get('Background') if w.node_tree else None
if bg:
bg.inputs['Color'].default_value = srgb(color)
bg.inputs['Strength'].default_value = strength
return w
def hdri(name='studio', strength=1.0, show=False):
"""블렌더에 들어 있는 HDRI 로 장면을 비춘다(CC0 · Poly Haven). name: studio · city · courtyard · forest · interior · night · sunrise · sunset.
show=False 면 빛과 반사에만 쓰고, 카메라에는 BACKDROP 색 배경이 보인다(Light Path 의 Is Camera Ray 로 가른다)."""
nt = bpy.context.scene.world.node_tree
nodes, links = nt.nodes, nt.links
env = nodes.new('ShaderNodeTexEnvironment')
env.image = bpy.data.images.load(os.path.join(bpy.utils.system_resource('DATAFILES'), 'studiolights', 'world', f'{name}.exr'))
bg = nodes['Background']
bg.inputs['Strength'].default_value = strength
links.new(env.outputs['Color'], bg.inputs['Color'])
if not show:
flat = nodes.new('ShaderNodeBackground')
flat.inputs['Color'].default_value = srgb(BACKDROP)
ray, mix = nodes.new('ShaderNodeLightPath'), nodes.new('ShaderNodeMixShader')
links.new(ray.outputs['Is Camera Ray'], mix.inputs['Fac'])
links.new(bg.outputs['Background'], mix.inputs[1])
links.new(flat.outputs['Background'], mix.inputs[2])
links.new(mix.outputs['Shader'], nodes['World Output'].inputs['Surface'])
return env
def look_at(obj, target):
obj.rotation_euler = (Vector(target) - obj.location).to_track_quat('-Z', 'Y').to_euler()
def camera(location=(7, -7, 5), target=(0, 0, 0.5), lens=50):
cam = bpy.data.objects.new('Camera', bpy.data.cameras.new('Camera'))
bpy.context.scene.collection.objects.link(cam)
cam.location = location
look_at(cam, target)
cam.data.lens = lens
bpy.context.scene.camera = cam
return cam
def frame(*objs, margin=1.15):
"""카메라 방향은 그대로 두고, objs 가 화면에 다 들어오게 카메라를 앞뒤로 옮긴다."""
sc = bpy.context.scene
dg = bpy.context.evaluated_depsgraph_get()
pts = [o.matrix_world @ Vector(c) for o in objs for c in o.evaluated_get(dg).bound_box]
mid = sum(pts, Vector()) / len(pts)
coords = [c for p in pts for c in (mid + (p - mid) * margin)]
loc, _scale = sc.camera.camera_fit_coords(dg, coords)
sc.camera.location = loc
return sc.camera
def light(kind='AREA', location=(4, -4, 6), target=(0, 0, 0), energy=600, size=3.0, color=(1, 1, 1)):
"""kind: POINT · SUN · SPOT · AREA. energy 는 와트(SUN 은 W/m²에 해당하는 세기)."""
data = bpy.data.lights.new(kind.title(), kind)
data.energy, data.color = energy, color
if kind == 'AREA':
data.size = size
obj = bpy.data.objects.new(data.name, data)
bpy.context.scene.collection.objects.link(obj)
obj.location = location
look_at(obj, target)
return obj
def studio(key=700, fill=200, rim=450):
"""기본 3점 조명(키·필·림). EEVEE·Cycles 용 — Workbench 는 자체 스튜디오 조명을 쓴다."""
return [light('AREA', (5, -4, 6), energy=key, size=4),
light('AREA', (-6, -3, 3), energy=fill, size=5),
light('AREA', (0, 6, 5), energy=rim, size=3)]
def material(name, color=CLAY, roughness=0.5, metallic=0.0, **inputs):
"""원리 BSDF 재질. inputs 는 소켓 이름(공백은 _ 로): Emission_Strength=3."""
mat = bpy.data.materials.new(name)
mat.diffuse_color = srgb(color) # Workbench 가 보는 색
bsdf = mat.node_tree.nodes['Principled BSDF']
bsdf.inputs['Base Color'].default_value = srgb(color)
bsdf.inputs['Roughness'].default_value = roughness
bsdf.inputs['Metallic'].default_value = metallic
for key, value in inputs.items():
bsdf.inputs[key.replace('_', ' ')].default_value = value
return mat
def paint(obj, mat):
obj.data.materials.clear()
obj.data.materials.append(mat)
return obj
def floor(size=80.0, color=None, z=0.0, wall=30.0, curve=6.0):
"""바닥과 뒷벽(+Y 쪽)이 둥글게 이어진 스튜디오 배경 — 지평선이 보이지 않는다. 카메라는 -Y 쪽에서 +Y 를 보게 둔다."""
import bmesh
back = size / 2
steps = 12
profile = [(-back, 0.0)] # (y, z) 단면: 앞 → 둥근 모서리 → 벽 위
for i in range(steps + 1):
a = (math.pi / 2) * i / steps
profile.append((back - curve + curve * math.sin(a), curve - curve * math.cos(a)))
profile.append((back, wall))
bm = bmesh.new()
left = [bm.verts.new((-back, y, z + h)) for y, h in profile]
right = [bm.verts.new((back, y, z + h)) for y, h in profile]
for i in range(len(profile) - 1):
bm.faces.new((left[i], right[i], right[i + 1], left[i + 1]))
me = bpy.data.meshes.new('Floor')
bm.to_mesh(me)
bm.free()
obj = bpy.data.objects.new('Floor', me)
bpy.context.scene.collection.objects.link(obj)
for poly in me.polygons:
poly.use_smooth = True
return paint(obj, material('Floor', color or BACKDROP, roughness=0.9))
def edges(obj, thickness=0.012, color=INK):
"""메시의 선(엣지)을 렌더에 보이게 한다 — 모디파이어까지 적용된 모양에 와이어프레임을 씌운 복제본."""
dg = bpy.context.evaluated_depsgraph_get()
wire = bpy.data.objects.new(obj.name + '.edges', bpy.data.meshes.new_from_object(obj.evaluated_get(dg)))
bpy.context.scene.collection.objects.link(wire)
wire.matrix_world = obj.matrix_world.copy()
mod = wire.modifiers.new('Wireframe', 'WIREFRAME')
mod.thickness, mod.use_even_offset = thickness, True
return paint(wire, material(obj.name + '.edges', color, roughness=0.8))
def shoot(name):
"""지금 장면을 <출력 폴더>/<항목>-<name>.webp 로 렌더한다."""
sc = bpy.context.scene
sc.render.filepath = os.path.join(OUT, f'{SLUG}-{name}.webp')
bpy.ops.render.render(write_still=True)
return sc.render.filepath
# ── 이 항목 ──
# 왼쪽 반만 만들고 미러 모디파이어로 오른쪽을 채운다.
import bmesh
reset('WORKBENCH')
camera((0, -8, 4.5), target=(0, 0, 1.0))
floor()
bpy.ops.mesh.primitive_cube_add(size=1.2, location=(0, 0, 1.0))
half = bpy.context.object
half.name = 'Half'
bm = bmesh.new()
bm.from_mesh(half.data)
bmesh.ops.subdivide_edges(bm, edges=bm.edges[:], cuts=3, use_grid_fill=True)
# x > 0 쪽 꼭짓점을 지워 왼쪽 절반만 남긴다(가운데 x = 0 줄은 남는다)
bmesh.ops.delete(bm, geom=[v for v in bm.verts if v.co.x > 0.001], context='VERTS')
for v in bm.verts: # 왼쪽 위를 날개처럼 끌어낸다
if v.co.x < -0.5 and v.co.z > 0:
v.co.x -= 0.7
v.co.z += 0.35
bm.to_mesh(half.data)
bm.free()
mirror = half.modifiers.new('Mirror', 'MIRROR')
mirror.use_axis = (True, False, False) # 오브젝트 원점의 X 축을 기준으로 뒤집어 붙인다
mirror.use_clip = True # 편집할 때 가운데 꼭짓점이 반대편으로 넘어가지 않게
mirror.use_mirror_merge = True # 가운데 겹친 꼭짓점을 하나로 합친다
subsurf = half.modifiers.new('Subdivision', 'SUBSURF')
subsurf.levels = subsurf.render_levels = 2
frame(half, margin=1.6) # 미러까지 켠 완성 모양에 맞춰 카메라를 둔다
for mod in half.modifiers: # 렌더에서만 잠시 끈다 — 모디파이어는 그대로 남아 있다
mod.show_render = False
paint(half, material('Clay', CLAY))
shoot('off')
for mod in half.modifiers:
mod.show_render = True
paint(half, material('Lime', LIME))
shoot('on')
얼굴·자동차·의자처럼 좌우가 같은 물체는 절반만 다듬으면 된다. 미러 모디파이어는 그 절반을 오브젝트 원점의 축을 기준으로 뒤집은 사본을 결과에만 덧붙인다. 원본 메시는 여전히 반쪽이라, 한쪽 꼭짓점을 옮기면 반대쪽이 실시간으로 따라온다. 화면에서는 속성 에디터의 렌치 탭(모디파이어) ▸ Add Modifier ▸ Generate ▸ Mirror 에 있다.
코드로는 obj.modifiers.new('Mirror', 'MIRROR') 한 줄이다. use_axis 로 X·Y·Z 중 뒤집을 축을, use_clip 으로 편집할 때 가운데 꼭짓점이 반대편으로 넘어가지 않게, use_mirror_merge 와 merge_threshold 로 가운데 겹친 꼭짓점을 하나로 합친다. 원점이 아닌 다른 기준을 쓰려면 mirror_object 에 엠프티 같은 오브젝트를 넣는다. 결과를 진짜 메시로 굳히려면 bpy.ops.object.modifier_apply(modifier='Mirror') 를 부른다.
가장 흔한 실수는 원점 위치다. 뒤집는 기준은 월드 중심이 아니라 그 오브젝트의 원점이라서, 원점이 이음매에 있지 않으면 두 반쪽이 겹치거나 벌어진다. 이 예시처럼 서브디비전을 미러 뒤에 두면 이음매까지 매끈하게 이어지고, 순서를 바꾸면 가운데에 주름이 생긴다(모디파이어는 위에서 아래로 계산된다).
언제 쓰나
좌우·상하 대칭인 모델을 처음 만들 때 바로 켠다. 비대칭 디테일(흉터·로고)을 넣을 단계가 되면 그때 적용(Apply)해서 메시로 굳힌다.