2D Normal Map으로 픽셀 아트 동적 조명 구현하기: 선명한 빛과 입체감 만들기

2D 픽셀 아트에서 평면적인 스프라이트를 2D Normal Map과 셰이더로 입체적으로 보이게 만드는 방법을 다룹니다. 노멀 디코딩, 점광원 감쇠, 픽셀 선명도 유지, 흔한 오류와 최적화 기준까지 단계별로 정리합니다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
2D Normal Map으로 픽셀 아트 동적 조명 구현하기: 선명한 빛과 입체감 만들기

2D Normal Map 기반 동적 조명은 스프라이트의 각 픽셀에 표면 방향을 저장하고 광원 방향과의 내적을 계산해 밝기를 결정하는 기법이다. 알베도 텍스처와 같은 해상도의 노멀 맵을 Point 필터로 샘플링하면 픽셀 아트의 선명한 윤곽을 유지하면서도 손전등, 횃불, 마법 효과에 반응하는 입체감을 만들 수 있다.

2D 픽셀 아트에 Normal Map 동적 조명이 필요한 이유

Unlit 머티리얼을 사용하는 일반적인 2D 스프라이트는 2D 광원에 반응하지 않는다. 반면 2D Normal Map은 벽돌의 돌출부, 금속 테두리, 캐릭터 얼굴의 방향처럼 픽셀 단위 표면 방향을 저장한다. 셰이더는 이 방향을 이용해 같은 스프라이트 안에서도 빛을 향한 면은 밝게, 반대 면은 어둡게 계산한다.

방식장점한계적합한 상황
단색 오버레이구현이 단순함표면 굴곡 표현이 없음화면 전체 명암 변화
라이트 마스크광원 범위를 제어하기 쉬움스프라이트 내부 입체감이 약함안개, 시야, 부드러운 원형 빛
2D Normal Map픽셀별 방향광 반응, 높은 입체감노멀 제작과 셰이더가 필요함횃불, 손전등, 마법, 금속 질감

2D Normal Map 적용 전후의 픽셀 아트 횃불 조명 비교

2D Normal Map은 어떤 원리로 밝기를 계산하는가?

Normal Map의 RGB 값은 표면 법선 벡터를 인코딩한다. 텍스처에서 읽은 rgb 값 범위는 0~1이므로 셰이더에서는 먼저 -1~1 범위로 변환하고 정규화한다.

N=normalize(2×normalRGB−1)N = normalize(2 \times normalRGB - 1)

점광원 위치를 lightPos, 현재 픽셀의 월드 위치를 worldPos라고 하면 빛 방향은 다음과 같다.

L=normalize(lightPos−worldPos)L = normalize(lightPos - worldPos)

확산광 밝기는 Lambert diffuse 모델의 내적으로 구한다.

diffuse=max(dot(N,L),0)diffuse = max(dot(N, L), 0)

여기에 거리 감쇠를 곱해 광원 반경 밖의 픽셀을 어둡게 만든다.

attenuation=saturate(1−distance(worldPos,lightPos)/lightRadius)attenuation = saturate(1 - distance(worldPos, lightPos) / lightRadius)

픽셀 아트에서는 부드러운 감쇠가 지나치게 흐릿하게 느껴질 수 있다. 이때 attenuation에 pow(attenuation, 2.0)를 적용하면 중심부가 또렷하고 가장자리가 빠르게 떨어지는 조명을 만들 수 있다.

픽셀 아트용 2D Normal Map은 어떻게 만들까?

1. 알베도와 같은 픽셀 그리드를 유지한다

노멀 맵은 원본 스프라이트와 가로세로 크기, 투명 영역, 피벗 기준이 일치해야 한다. 예를 들어 32×32 캐릭터 스프라이트에는 32×32 노멀 맵을 사용한다. 보간을 사용하는 리사이즈나 블러는 인접 픽셀의 방향 정보를 섞어 픽셀 아트 특유의 단단한 인상을 약화시킬 수 있다.

2. 돌출 방향을 색으로 기록한다

일반적인 탄젠트 공간 노멀 맵에서 기본 평면은 대략 (128, 128, 255)에 가까운 보라색이다. 오른쪽을 향한 면은 빨간색 성분을 높이고 왼쪽을 향한 면은 낮춘다. 위쪽 면은 초록색 성분을 높이는 관례가 흔하지만 사용 중인 툴과 셰이더의 Y축 방향이 같은지 반드시 확인해야 한다.

표면 방향노멀 벡터 예시RGB 경향
정면(0, 0, 1)보라색 계열
오른쪽(1, 0, 0)빨간색 증가
왼쪽(-1, 0, 0)빨간색 감소
위쪽(0, 1, 0)초록색 증가

3. 픽셀 단위로 수정하고 즉시 검증한다

Aseprite 같은 픽셀 편집기에서 별도 레이어 또는 별도 파일로 노멀 맵을 관리하면 대응 픽셀을 확인하기 쉽다. 자동 생성 도구는 초안 제작에 활용하고, 특히 작은 윤곽과 계단형 경계는 확대해 결과를 확인한 뒤 필요하면 수동으로 다듬는다.

픽셀 단위 노멀 맵에서 보라색 기본면과 방향별 색상 변화를 설명하는 예시

Unity URP에서 2D Normal Map 동적 조명을 구현하는 순서

Unity 2022.3 LTS와 URP 2D Renderer를 사용한다면, 먼저 Sprite Renderer에 Sprite-Lit-Default처럼 2D Light에 반응하는 Lit 머티리얼을 지정한다. Unity 문서의 2D 조명용 스프라이트 준비 안내는 이 기본 머티리얼이 2D Light에 반응한다고 설명한다. 커스텀 효과가 필요할 때는 아래처럼 자체 셰이더 또는 2D Sprite Lit Shader Graph에서 같은 조명 계산을 확장할 수 있다.

1. 텍스처 임포트 설정을 맞춘다

  • 알베도와 노멀 맵의 Filter Mode를 Point (no filter)로 설정한다.
  • 노멀 맵은 Unity의 Texture Type: Normal map으로 임포트한다.
  • 아틀라스를 쓴다면 패딩을 확보하고 Mip Maps는 일반적으로 끈다.
  • Pixel Perfect Camera의 Pixel Snapping을 활성화하고 필요하면 정수 배율 렌더링을 사용해 화면 이동 중 서브픽셀 블러를 줄인다.

2. 노멀과 광원 벡터를 계산한다

다음 코드는 이미 월드 공간으로 변환한 법선과 위치를 입력받아 핵심 Lambert diffuse 계산만 수행하는 HLSL 예시다. Unity의 Normal map 임포트 텍스처는 저장 형식에 맞는 UnpackNormal 계열 함수로 먼저 디코딩하고, 법선을 월드 공간으로 변환해야 한다. Unity도 노멀 맵 셰이더 예시에서 UnpackNormal로 텍스처를 디코딩한 뒤 탄젠트 공간 법선을 월드 공간으로 변환한다. 실제 URP 프로젝트에서는 Sprite Lit 셰이더의 좌표계와 조명 데이터 전달 방식에 맞게 worldPos, normalWS, 광원 파라미터를 연결한다. lightRadius는 0보다 큰 값이어야 한다.

float3 ApplyPointLight(
    float3 albedo,
    float3 normalWS,
    float3 worldPos,
    float3 lightPos,
    float3 lightColor,
    float lightRadius,
    float ambientStrength)
{
    normalWS = normalize(normalWS);
    float3 toLight = lightPos - worldPos;
    float distanceToLight = length(toLight);
    float3 lightDir = toLight / max(distanceToLight, 0.0001);

    float diffuse = saturate(dot(normalWS, lightDir));
    float attenuation = saturate(1.0 - distanceToLight / lightRadius);
    attenuation *= attenuation;

    float3 lighting = ambientStrength + lightColor * diffuse * attenuation;
    return albedo * lighting;
}

3. 광원 파라미터를 게임플레이와 연결한다

횃불은 lightRadius와 lightColor를 시간에 따라 작게 흔들면 정적인 원형 조명보다 자연스럽다. 단, 매 프레임 난수를 크게 바꾸면 픽셀 아트가 깜빡이는 것처럼 보인다. 낮은 주파수의 Perlin Noise 또는 보간된 난수를 사용하고 반경 변화 폭은 기본 반경의 3~8% 정도부터 조절한다.

float flicker = Mathf.PerlinNoise(Time.time * 2f, 0f);
float radius = baseRadius * Mathf.Lerp(0.95f, 1.05f, flicker);
lightMaterial.SetFloat("_LightRadius", radius);

빛이 평평하거나 반대로 보일 때 무엇을 점검해야 할까?

먼저 점검할 항목 중 하나는 좌표계 불일치다. 노멀 맵의 초록 채널 방향과 셰이더가 기대하는 Y축 방향이 다르면 위아래 굴곡이 반전된다. Unity에서 노멀 맵을 임포트한 뒤에도 문제가 보이면 셰이더에서 normalWS.y *= -1.0;를 시험하기 전에 원본 노멀 맵의 채널 방향과 대상 렌더링 파이프라인의 규칙을 먼저 확인한다.

증상원인 후보해결 방법
빛이 아래에서 비추는 것처럼 보임Green 채널 반전노멀 맵 또는 셰이더의 Y축 규칙을 통일
스프라이트가 흐릿함Bilinear 필터 또는 Mip MapPoint 필터 적용, 불필요한 Mip Map 비활성화
윤곽이 검게 깨짐투명 픽셀의 노멀 값 문제알파 클리핑 전후 순서와 투명 영역의 노멀 확인
조명이 지나치게 딱딱함감쇠와 diffuse만 사용약한 ambient, rim 또는 색 보간 추가
여러 조명에서 성능 저하픽셀당 광원 반복 계산광원 수 제한, 타일/클러스터 컬링 또는 라이트 마스크 병행

성능을 유지하면서 동적 조명을 연출하는 방법

픽셀별 조명 셰이더에서는 조명이 적용되는 각 픽셀에서 계산이 수행된다. 화면 전체에 많은 점광원을 중첩하면 광원 수가 성능 병목 후보가 될 수 있으므로 프로파일러로 확인한다.

  1. 게임플레이에 중요한 광원만 실시간 계산한다. 장식용 창문빛이나 멀리 있는 횃불은 베이크된 발광 스프라이트로 대체한다.
  2. 각 광원의 반경을 필요한 범위로 제한한다. 반경이 커질수록 영향을 받는 화면 픽셀이 증가한다.
  3. 조명 색상, 반경, 강도 변화는 머티리얼 인스턴스 남발 대신 가능한 한 공용 머티리얼과 셰이더 파라미터 체계로 관리한다.
  4. 프로파일러에서 GPU 프레임 시간과 드로우 콜을 함께 확인한다. 광원 수를 줄였는데도 개선이 없다면 오버드로우나 투명 스프라이트 레이어가 원인일 수 있다.

자주 묻는 질문 (FAQ)

2D Normal Map은 모든 픽셀 아트에 필요한가?

아니다. 광원 이동이 게임의 분위기나 정보 전달에 중요할 때 효과가 크다. 밝기 변화만 필요하다면 라이트 마스크나 컬러 오버레이가 더 단순하고 경제적이다.

노멀 맵은 원본보다 큰 해상도로 만들어야 하는가?

아니다. 픽셀 아트에서는 대개 원본과 같은 해상도가 적절하다. 더 큰 노멀 맵은 실제 형태 정보가 늘기보다 보간과 관리 비용만 증가시키기 쉽다.

여러 색의 광원을 동시에 적용할 수 있는가?

가능하다. 각 광원의 lightColor * diffuse * attenuation 결과를 누적하면 된다. 다만 누적값이 과도하게 밝아지지 않도록 HDR 톤 매핑, 최대 밝기 제한 또는 의도적인 색 보정 기준을 정해야 한다.

정리

2D Normal Map 동적 조명의 핵심은 픽셀 아트의 해상도와 샘플링 규칙을 지키면서 법선 벡터, 광원 방향, 거리 감쇠를 일관된 좌표계에서 계산하는 것이다. 먼저 Point 필터와 올바른 노멀 방향을 검증한 뒤 소수의 핵심 광원에만 적용하면 선명함과 성능을 함께 확보할 수 있다.