며칠 전 라이팅 리뷰 시간에 표면이 이상하게 울퉁불퉁해 보이는 에셋이 하나 있었었습니다. 범인은 다름 아닌 노멀맵(Normal Map)의 컬러스페이스, 즉 sRGB로 잘못 불러온 텍스처였습니다. 오늘은 왜 이런 일이 생기는지 차근차근 풀어볼게요.
노멀맵의 RGB는 색이 아니라 좌표값입니다 🧭
우리는 보통 텍스처의 RGB 채널을 색으로 생각합니다. 빨간 채널이 높으면 붉은 느낌, 파란 채널이 높으면 파란 느낌을 상상하죠. 그런데 노멀맵은 예외입니다. 노멀맵의 R, G, B 채널은 각각 표면이 향하고 있는 방향, 즉 3차원 벡터의 X, Y, Z 값을 담고 있어요.
벡터는 원래 -1에서 1 사이의 값을 가지는데, 이미지 파일은 보통 0에서 1 사이의 값(8비트 기준으로는 0~255)만 저장할 수 있습니다. 그래서 게임 엔진이나 렌더러는 벡터 값을 이미지에 저장할 수 있도록 간단한 공식으로 압축합니다.
저장값 = (벡터값 + 1) / 2
이 공식을 거치면 -1은 0, 0은 0.5, 1은 1로 변환됩니다. 즉 노멀맵의 픽셀 값은 “밝기”가 아니라 “이 좌표축으로 표면이 얼마나 기울어져 있는가”를 나타내는 숫자 데이터인 셈이에요. 사진 파일 형식을 빌려서 벡터 데이터를 담아 놓은, 말하자면 위장한 데이터 파일이라고 생각하시면 됩니다.
노멀맵이 유독 보라색인 이유
노멀맵을 열어보면 전체적으로 은은한 보라색 톤이 도는 걸 본 적 있으실 거예요. 이건 디자인이 아니라 수학의 결과입니다. 아무런 굴곡이 없는 완전히 평평한 면은 카메라를 정면으로 바라보는 벡터, 즉 (X=0, Y=0, Z=1)을 가집니다.
이걸 위 공식으로 인코딩하면 이렇게 됩니다.
- X = 0 → (0+1)/2 = 0.5 → R 채널 128
- Y = 0 → (0+1)/2 = 0.5 → G 채널 128
- Z = 1 → (1+1)/2 = 1.0 → B 채널 255
R과 G가 중간값(128)이고 B가 최댓값(255)에 가까운 색, 그게 바로 우리가 아는 그 은은한 보라색입니다. 다시 말해 노멀맵에서 보라색은 “평평하고 아무 디테일도 없는 면”을 뜻하는 기준점입니다. 표면에 굴곡이 생기면 이 기준 색에서 조금씩 어긋난 색으로 바뀌면서 미세한 요철을 표현하게 되는 거예요.
sRGB로 읽으면 왜 벡터가 뒤틀릴까: 감마 디코딩의 함정
여기서 문제가 생깁니다. 렌더러나 게임 엔진이 텍스처를 불러올 때는 그 텍스처가 sRGB 색공간인지, 아니면 Raw(또는 Linear, Non-Color) 데이터인지를 지정해줘야 해요. sRGB로 지정하면 소프트웨어는 자동으로 감마 디코딩이라는 보정을 적용합니다.
감마 디코딩은 원래 사람 눈이 어두운 영역의 밝기 차이를 더 예민하게 느낀다는 특성 때문에 생긴 규칙입니다. 카메라나 모니터가 색을 저장하고 표시할 때 어두운 영역에 더 많은 정보를 몰아주기 위해 픽셀 값에 감마 곡선(대략 2.2제곱)을 씌우는 건데, 색을 다루는 이미지에서는 이게 꼭 필요한 과정이에요.
그런데 노멀맵은 색이 아니라 좌표값이라고 했죠. 여기에 감마 디코딩을 적용하면 벡터의 숫자 자체가 완전히 다른 값으로 바뀌어 버립니다. 실제로 계산해보면 이렇습니다.
0.5 를 sRGB로 읽고 감마 디코딩(약 2.2제곱)을 적용하면
0.5 ^ 2.2 ≈ 0.218
방금 “평평한 면”을 나타내던 X값 0.5가 감마 디코딩을 거치면 약 0.218로 뚝 떨어집니다. 이걸 다시 벡터로 되돌리면(0.218 × 2 − 1 ≈ -0.564) 원래는 0이어야 할 X축 방향이 -0.564라는 엉뚱한 기울기로 계산돼 버려요. 평평했던 면이 갑자기 한쪽으로 심하게 기울어진 표면처럼 취급되는 겁니다. 노멀맵은 절대 색이 아니므로 감마 보정 없이 저장된 숫자를 그대로, 있는 그대로 읽어야만 벡터 계산이 맞아떨어집니다.
sRGB로 잘못 불러왔을 때 나타나는 증상들
현장에서 흔히 마주치는 증상을 정리하면 아래와 같아요.
- 디테일 과장 또는 약화: 굴곡이 실제보다 훨씬 심하게 보이거나, 반대로 밋밋하게 뭉개져 보입니다.
- 라이팅 방향이 이상함: 빛이 예상과 다른 방향에서 오는 것처럼 하이라이트가 엉뚱한 곳에 생깁니다.
- 엣지가 뭉개지거나 계단 현상 발생: 특히 어두운 값 영역(0에 가까운 값들)이 감마 디코딩으로 더 크게 뭉개지면서 미세한 디테일이 뭉개집니다.
- 노멀 벡터 정규화가 깨짐: 벡터의 길이가 1이어야(단위 벡터) 정상인데, 왜곡된 값 때문에 길이가 들쭉날쭉해져 셰이딩 계산 전체가 흔들립니다.
- 머티리얼마다 느낌이 달라짐: 같은 노멀맵인데 렌더러나 엔진 설정에 따라 결과가 다르게 나와서 원인 파악이 어려워집니다.
문제는 이 증상들이 “약간 이상한데 뭐가 문제인지는 모르겠다” 수준으로 은근슬쩍 나타난다는 점이에요. 완전히 깨지는 게 아니라 미묘하게 어색해서, 라이팅 세팅이나 다른 텍스처 탓으로 오해하기 쉽습니다.
탄젠트 스페이스 vs 월드 스페이스, 그리고 그린 채널 반전
노멀맵을 다루다 보면 한 번쯤 마주치는 개념이 탄젠트 스페이스(Tangent Space)와 월드 스페이스(World Space)입니다. 간단히 비유하면, 월드 스페이스 노멀맵은 “지구본 위의 절대 좌표”처럼 오브젝트가 어떻게 회전하든 항상 같은 기준(고정된 좌표축)으로 벡터를 표현합니다. 그래서 초록빛, 파란빛이 뒤섞인 화려한 색으로 보이고, 오브젝트가 움직이거나 애니메이션되면 값이 다시 계산되어야 해서 캐릭터처럼 변형되는 에셋에는 잘 쓰지 않아요.
반면 탄젠트 스페이스 노멀맵은 “그 표면 자체를 기준으로 한 상대 좌표”라서, 오브젝트가 어떻게 움직이든 표면 위의 굴곡 정보만 담고 있습니다. 그래서 우리가 흔히 보는 보라색 노멀맵은 대부분 탄젠트 스페이스이고, 애니메이션되는 캐릭터나 재사용되는 에셋에 널리 쓰입니다.
여기에 하나 더 챙겨야 할 이슈가 있어요. 바로 그린(G) 채널의 방향입니다. DirectX 기반 툴(Substance Painter 기본값, 언리얼 엔진 등)은 Y축을 아래 방향(-Y)으로 정의하고, OpenGL 기반 툴(Maya, Blender, Unity 등)은 Y축을 위 방향(+Y)으로 정의합니다. 이 둘은 그린 채널이 정확히 반전된 관계예요. 만약 DirectX용으로 만든 노멀맵을 OpenGL 기준 렌더러에 그대로 넣으면 굴곡이 움푹 파인 것처럼 보이거나 반대로 볼록 튀어나온 것처럼 뒤집혀 보입니다. 컬러스페이스 문제와는 별개의 이슈지만, 노멀맵이 이상하게 보일 때 함께 체크해봐야 할 포인트입니다.
올바른 설정법과 확인 방법
그렇다면 실제로 어떻게 설정해야 할까요? 핵심은 딱 하나, “노멀맵은 색이 아니라 데이터이니 감마 보정 없이 읽어라”입니다. 소프트웨어마다 이 옵션의 이름은 다르지만 의미는 같습니다.
| 항목 | 잘못된 설정 (sRGB) | 올바른 설정 (Raw / Linear / Non-Color) |
|---|---|---|
| 컬러스페이스 | sRGB, sRGB Texture | Raw, Linear, Non-Color, Utility – Raw |
| 값 처리 방식 | 감마 디코딩 적용 (제곱 연산) | 감마 보정 없이 저장된 값 그대로 사용 |
| 0.5 (128) 값의 결과 | 약 0.218로 왜곡 | 0.5로 정확히 유지 |
| 평평한 면의 결과 | 미세하게 기울어진 것으로 오인 | 정확히 평평하게 계산 |
| 대표 증상 | 디테일 과장/뭉개짐, 이상한 하이라이트 | 정상적인 셰이딩 |
소프트웨어별로 확인할 위치는 대략 이렇습니다.
- Maya / Arnold: File 노드의 Color Space를 Raw 또는 Utility – Raw로 설정
- Substance Painter, Photoshop 등에서 익스포트 시: 노멀맵은 항상 8비트보다 16비트로 저장하는 것을 권장 (양자화 오류 감소)
- Unreal Engine: 텍스처 임포트 시 Compression Settings를 Normalmap으로 지정하면 sRGB 체크가 자동으로 꺼짐
- Unity: Texture Type을 Normal Map으로 지정하면 자동으로 Linear 처리됨
- Blender: 이미지 텍스처 노드의 Color Space를 Non-Color로 설정
확인하는 가장 쉬운 방법은 완전히 평평한 테스트 평면에 노멀맵을 적용해보는 거예요. 설정이 맞다면 평면은 정말로 평평하게 셰이딩되고, sRGB로 잘못 읽혔다면 뭔가 미세하게 굴곡진 것처럼 하이라이트가 왜곡되어 보입니다.
이미 잘못 세팅된 씬을 점검하는 팁
이미 작업이 많이 진행된 씬이라면 하나씩 다시 임포트하기보다 아래 순서로 점검해보세요.
- 텍스처 노드의 컬러스페이스 속성을 전수 확인: sRGB로 되어 있는 노멀맵 슬롯이 있는지 씬 전체를 스크립트나 렌더러의 텍스처 목록 기능으로 검색합니다.
- 의심되는 오브젝트에 회색 매트 셰이더 + 스튜디오 라이팅으로 테스트 렌더: 색상 정보를 배제하고 순수하게 셰이딩만 확인하면 왜곡을 더 쉽게 발견할 수 있습니다.
- 파일명 규칙을 활용한 자동화: “_normal”, “_nrm” 같은 접미사가 붙은 텍스처는 자동으로 Raw로 지정되도록 파이프라인 스크립트나 임포트 프리셋을 만들어두면 재발을 막을 수 있습니다.
- 그린 채널 반전 여부도 함께 체크: 컬러스페이스를 고쳤는데도 여전히 이상하다면 DirectX/OpenGL 컨벤션 불일치를 의심해보세요.
한 번 파이프라인에 규칙을 심어두면 이후로는 사람이 매번 신경 쓰지 않아도 되니, 초반에 시간을 들여서라도 자동화해두는 걸 추천합니다.
자주 묻는 질문(FAQ)
Q1. 노멀맵을 8비트로 저장해도 괜찮나요? 가능은 하지만 권장하지는 않습니다. 8비트는 256단계밖에 표현하지 못해서 미세한 경사 변화가 계단처럼 뭉개지는 양자화 현상이 생기기 쉬워요. 특히 부드러운 곡면에서는 16비트로 저장하는 편이 훨씬 자연스러운 결과를 줍니다.
Q2. 러프니스나 메탈릭 맵도 노멀맵처럼 Raw로 설정해야 하나요? 네, 맞습니다. 러프니스, 메탈릭, 오클루전 맵 역시 색이 아니라 수치 데이터를 담고 있어서 sRGB가 아닌 Raw나 Linear로 불러와야 합니다. 이 주제는 다음 글에서 좀 더 자세히 다룰 예정이에요.
Q3. sRGB로 잘못 불러온 걸 렌더 후에 보정으로 되돌릴 수 있나요? 근본적으로는 어렵습니다. 감마 디코딩이 셰이딩 계산 이전, 즉 벡터 값을 만드는 단계에서 이미 왜곡을 일으키기 때문에 최종 이미지에 후보정을 해도 원래의 정확한 셰이딩 결과로 되돌릴 수 없어요. 텍스처 노드의 컬러스페이스 설정 자체를 고치는 것이 유일하게 확실한 해결책입니다.
