범프 노드

Bump Node

높이 신호의 기울기로 음영 법선을 흔들어 메시를 늘리지 않고 요철처럼 보이게 한다.

다른 이름: BumpShaderNodeBumpBump Mapping
···
bpy — 이 항목
# 낮은 각도의 빛이 타일 범프 높낮이를 그림자와 하이라이트로 드러내게 한다.
reset('EEVEE', samples=32)
camera((3.4, -5.2, 2.1), target=(0, 0, 0.13), lens=50)
floor()
world_color(BACKDROP, strength=0.18)
light('AREA', (-3, -1.8, 1.5), energy=1000, size=0.8)

bpy.ops.mesh.primitive_cube_add(size=2.35, location=(0, 0, 0.13))
tile = bpy.context.object
tile.scale.z = 0.12
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bevel = tile.modifiers.new('Tile edge', 'BEVEL')
bevel.width, bevel.segments = 0.06, 3
mat = material('Bump', LIME, roughness=0.32)
paint(tile, mat)
nodes, links = mat.node_tree.nodes, mat.node_tree.links
noise, bump = nodes.new('ShaderNodeTexNoise'), nodes.new('ShaderNodeBump')
noise.inputs['Scale'].default_value = 13.0
bump.inputs['Distance'].default_value = 0.055
links.new(noise.outputs['Fac'], bump.inputs['Height'])
links.new(bump.outputs['Normal'], nodes['Principled BSDF'].inputs['Normal'])
frame(tile, margin=1.1)

for strength in (0.0, 0.5, 1.0):
    bump.inputs['Strength'].default_value = strength
    shoot(f'b{int(strength * 100):02d}')
전체 스크립트 — Blender 5.1.1 에서 그대로 실행
# bump-node — Blender 5.1.1 에서 그대로 돌아가는 스크립트
# 실행: blender -b --factory-startup --python bump-node.py -- <출력 폴더>
SLUG = 'bump-node'

# ── 무대: 모든 항목이 같이 쓰는 준비 코드 (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


# ── 이 항목 ──
# 낮은 각도의 빛이 타일 범프 높낮이를 그림자와 하이라이트로 드러내게 한다.
reset('EEVEE', samples=32)
camera((3.4, -5.2, 2.1), target=(0, 0, 0.13), lens=50)
floor()
world_color(BACKDROP, strength=0.18)
light('AREA', (-3, -1.8, 1.5), energy=1000, size=0.8)

bpy.ops.mesh.primitive_cube_add(size=2.35, location=(0, 0, 0.13))
tile = bpy.context.object
tile.scale.z = 0.12
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
bevel = tile.modifiers.new('Tile edge', 'BEVEL')
bevel.width, bevel.segments = 0.06, 3
mat = material('Bump', LIME, roughness=0.32)
paint(tile, mat)
nodes, links = mat.node_tree.nodes, mat.node_tree.links
noise, bump = nodes.new('ShaderNodeTexNoise'), nodes.new('ShaderNodeBump')
noise.inputs['Scale'].default_value = 13.0
bump.inputs['Distance'].default_value = 0.055
links.new(noise.outputs['Fac'], bump.inputs['Height'])
links.new(bump.outputs['Normal'], nodes['Principled BSDF'].inputs['Normal'])
frame(tile, margin=1.1)

for strength in (0.0, 0.5, 1.0):
    bump.inputs['Strength'].default_value = strength
    shoot(f'b{int(strength * 100):02d}')

매끈한 종이에 밝고 어두운 선을 그어 손으로 만져지는 것처럼 보이게 하는 효과다. 범프는 메시의 점을 옮기지 않고 음영 계산에 쓰는 법선만 바꾼다. 화면에서는 셰이더 에디터 ▸ Add ▸ Displacement ▸ Bump에서 노드를 추가한다.

코드에서는 nodes.new('ShaderNodeBump')를 만들고 Height에 흑백 신호를 잇는다. Bump의 Normal 출력을 원리 BSDF의 Normal 입력에 연결한다. Strength는 효과의 비중, Distance는 높이 신호가 표면 기울기에 미치는 크기다. 노멀 맵을 입력할 때는 이 노드 대신 Normal Map 노드를 쓴다.

범프는 실루엣을 바꾸지 않고, 반사 하이라이트와 음영에서만 요철을 읽게 한다. Distance가 과하면 요철이 과장되고 거칠어진다. 안쪽으로 파이게 하려면 Invert를 켠다 — 이 예시는 낮은 각도의 빛을 납작한 요철 타일에 비추고 Strength를 비교한다.

언제 쓰나

미세한 가죽결·주름·흠집처럼 실루엣은 유지하며 표면 음영만 거칠게 만들 때 쓴다. 실제 윤곽 변화가 필요하면 자매 항목의 디스플레이스먼트를 본다.

페이지로 열기 ↗

항목 추가 요청

아틀라스에 없는 블렌더 용어·도구·작업 흐름을 알려 주세요. 하나씩 보고 추가합니다.