본문으로 건너뛰기

노멀맵 변환 · OpenGL ↔ DirectX (초록 채널 반전)

두 규약의 차이는 초록 채널 하나입니다. 변환은 255 − g이며 그 자체가 역변환이라, 변환한 맵을 다시 변환하면 같은 바이트가 됩니다.

OpenGL 방식 노멀맵(Y+, 초록이 위쪽 — Unity·Godot·glTF가 기대하는 방식)과 DirectX 방식(Y−, 초록이 아래쪽)은 초록 채널만 다릅니다. 한쪽을 다른 쪽으로 바꾸는 것은 모든 텍셀의 g → 255 − g이며 빨강·파랑·알파는 그대로이고, 두 번 하면 원래 바이트로 돌아옵니다. Nerulio 변환기는 브라우저에서 PNG에 정확히 이 계산을 적용하고 단위 길이 점검을 보여 줍니다. 파일이 어느 규약인지는 추측하지 않으며, 그 판별은 측정 근거가 있는 Studio 텍스처 작업 공간의 감지기가 합니다.

텍스처 맵을 끌어다 놓으세요(PNG는 채널 바이트를 그대로 유지)
브라우저에서 처리하며 파일은 업로드되지 않습니다.
텍스처 랩: CC0 텍스처로 만든 노멀맵과 OpenGL/DirectX 규약 표시
CC0 에셋(ambientCG)을 연 실제 텍스처 랩 화면입니다.
출력
  • Channel PNGs측정 확인· numpy
  • Normal map측정 확인· numpy
  • Packed mask / ORM측정 확인· Pillow
  • Engine import미검증

초록 채널의 의미와 반전의 효과

탄젠트 공간 노멀맵은 텍셀마다 단위 벡터 하나를 저장합니다. 빨강은 X(이미지 오른쪽), 초록은 Y, 파랑은 Z(표면 바깥)이며 각각 (n + 1) / 2 × 255로 인코딩합니다. 평평한 텍셀은 (128, 128, 255)입니다. 두 규약은 X와 Z에서는 같고, 이미지에서 +Y가 어느 쪽인지만 다릅니다. OpenGL은 위, DirectX는 아래입니다.

반대 규약의 맵을 넣으면 세로 방향 기울기가 모두 뒤집힙니다. 위에서 비추면 요철의 위쪽이 아니라 아래쪽 가장자리가 밝아져 튀어나온 곳이 들어가 보이거나 빛이 아래에서 오는 듯 보입니다. 가로 기울기는 맞기 때문에 텍스처 문제가 아니라 조명 문제처럼 보이기도 합니다.

해결은 다시 굽는 것이 아니라 산수입니다. y → −y는 g → 255 − g이고, 이 연산은 자기 자신이 역연산입니다. Unity 매뉴얼은 Unity가 Y+(OpenGL) 노멀맵을 쓴다고, Godot 매뉴얼은 X+·Y+·Z+("OpenGL 방식")가 필요하다고 적고, glTF 2.0은 +X 오른쪽·+Y 위쪽으로 정합니다. Unreal은 Epic이 간접적으로만 밝히기 때문에 랩에서 DirectX(−Y)에 "추정" 표시를 붙입니다.

이 파일은 어느 규약일까? 둘 다 올바른 노멀맵이라 랩은 추측하지 않습니다. Studio 텍스처 작업 공간은 표면 자체(기울기들이 서로 모순 없는지, 스프라이트라면 실루엣까지)로 추정해 신뢰도와 함께 보여 주며, 확인하기 전에는 판정을 적용하지 않습니다. 이 점검은 OpenGL인가 DirectX인가를 보세요.

탄젠트 공간
표면 기준의 노멀. X는 텍스처 U, Y는 V 방향, Z는 표면 바깥.
OpenGL (Y+)
이미지에서 초록이 위쪽. Unity·Godot·glTF.
DirectX (Y−)
이미지에서 초록이 아래쪽.
단위 길이
올바른 노멀은 길이 1인 벡터로 디코딩됩니다. 랩은 평균과 최대 오차를 보고합니다.

예시: 텍셀 하나와 감지 성능

이미지 위쪽으로 기운 텍셀, 노멀 (0, 0.6, 0.8)과 평평한 텍셀:

OpenGL   (0, 0.6, 0.8)  → (128, 204, 230)      평평 (128, 128, 255)
DirectX  (0, −0.6, 0.8) → (128,  51, 230)      평평 (128, 127, 255)
초록     255 − 204 = 51 → (51 − 127.5) / 127.5 = −0.6

규약을 아는 맵에서 Studio GL/DX 감지기 (2026-09-24):
  1K 전체 맵 128장      판정 100 %    정확도 100 %     틀린 "높음" 0
  64² 잘라낸 조각 504개  판정 99.6 %   정확도 99.6 %    틀린 "높음" 0
  단순 규칙 "초록이 대체로 높으면 OpenGL", 표본 1562개: 46 %

평평한 텍셀은 (128, 127, 255)가 되고 다시 변환하면 (128, 128, 255)로 돌아옵니다. 한 단계 차이는 8비트 인코딩의 중심 때문이지 손실이 아닙니다. 투명한 가장자리가 없으면 빨강이 뒤집힌 맵은 감지기에서 DirectX로 읽히며, Studio는 그 사실을 알려 줍니다.

채널마다 바뀌는 것

채널OpenGL (Y+)변환기DirectX (Y−)
R (X)이미지 오른쪽그대로이미지 오른쪽
G (Y)이미지 위쪽255 − g이미지 아래쪽
B (Z)표면 바깥, 128 이상그대로표면 바깥, 128 이상
A파일에 있던 값그대로그대로
ambientCG 파일 이름_NormalGL—_NormalDX

Studio 텍스처 작업 공간은 파일 이름의 _NormalGL·_NormalDX를 선언된 규약으로 읽습니다. 랩은 둘 다 그냥 노멀맵으로 분류하며, 그 밖의 이름은 규약에 대해 아무것도 알려 주지 않습니다.

이렇게 작동합니다

  • 초록 채널만 바꾸는 OpenGL ↔ DirectX 변환(채널별로 확인).
  • 어느 엔진이 어떤 규약을 기대하는지와 그 근거 문서(Unreal의 −Y는 추정으로 표시).
  • 노멀맵 검증: 평균 길이, 최대 오차, 가장 낮은 파랑.
  • Sobel·Scharr·Sobel 5×5와 반복 텍스처용 wrap으로 높이 → 노멀 생성.

사용 방법

  1. 노멀맵을 끌어다 놓습니다.
  2. "무엇을 할까요"에서 OpenGL ↔ DirectX를 고릅니다.
  3. 검증 수치를 확인합니다.
  4. 변환한 맵을 받습니다.

받게 되는 파일

파일용도
rock_normal-directx.png변환한 맵. 이름은 입력이 OpenGL이라고 가정합니다. DirectX 맵을 변환했다면 내용은 OpenGL이므로 헷갈리지 않게 이름을 바꾸세요.
texture-report.json선택: 점검 단계에서 세트의 노멀맵마다 normalCheck.meanLength, unitLength, blueNonNegative.

변환한 맵을 엔진에서 쓰기

엔진 설정은 각 문서를 따른 것이며 Nerulio가 이 가져오기를 실행하지는 않았습니다.

  1. Unity 6: OpenGL 맵을 가져와 Texture Type을 Normal map으로 둡니다. DirectX 맵만 있다면 변환하지 않고 같은 임포터의 Flip Green Channel을 체크해도 됩니다.
  2. Godot 4: 머티리얼의 Normal Map을 켜고 OpenGL 맵을 지정합니다. DirectX 맵이라면 가져오기 독에서 Process › Normal Map Invert Y를 켜고 Reimport하는 방법도 있습니다.
  3. glTF 2.0: 노멀 텍스처는 +X 오른쪽·+Y 위쪽이어야 하므로 DirectX 맵은 내보내기 전에 변환합니다.
  4. DirectX 방식 대상: 변환한 -directx.png를 쓰고 그 엔진의 가져오기 문서를 따릅니다.
  5. 머티리얼을 위에서 비추고 튀어나온 부분의 위쪽 가장자리가 밝은지 확인합니다.

결과 확인

  • 노멀 단계에 평균 벡터 길이와 최대 오차가 나옵니다. 올바른 맵은 1에 가깝습니다.
  • 저장한 파일을 원본과 채널별로 비교합니다(채널 단계). R·B·A는 같고 G = 255 − g여야 합니다.
  • 결과를 한 번 더 변환하면 원래 바이트가 돌아옵니다.

문제 해결

증상유력한 원인확인 방법해결
튀어나온 곳이 들어가 보이고 빛이 아래에서 오는 듯함맵이 반대 규약위에서 비출 때 어느 가장자리가 밝은가여기서 변환하거나 Unity Flip Green Channel·Godot Normal Map Invert Y 사용
변환했는데 좌우 방향 조명이 여전히 틀림초록이 아니라 빨강이 뒤집힘(NormalMap-Online 기본 출력이 이런 맵)Studio 검사 패널이 스프라이트에서 빨강 반전을 알려 줌Studio 텍스처 작업 공간의 Flip red (X−). 랩은 초록만 변환
원래 맞았는데 변환 후 틀어짐맵이 이미 목표 규약이었음한 번 더 변환하면 되돌아감(자기 자신이 역연산)원본을 그대로 쓰기
Unity에서 조명이 밋밋하거나 번져 보임맵을 Default 색 텍스처로 가져옴인스펙터의 Texture TypeTexture Type을 Normal map으로
검사에서 파랑이 128 미만이거나 벡터 길이가 1에서 멀다고 나옴탄젠트 공간 노멀맵이 아님(하이트 맵, 오브젝트 공간 맵, 크기를 바꾼 색 사본)노멀 단계의 평균 길이와 가장 낮은 파랑초록 반전으로는 안 고쳐짐. 노멀맵 생성기로 하이트에서 만들기

다른 방법

방법이럴 때 더 낫습니다
Unity Flip Green Channel / Godot Normal Map Invert Y엔진 하나에서만 쓸 때. 가져오기 옵션이 두 번째 파일 없이 반전합니다.
다른 버전 내려받기에셋 사이트가 둘 다 줄 때(ambientCG는 _NormalGL과 _NormalDX). 엔진이 기대하는 쪽을 받으세요.
Studio의 규약 점검맵이 어느 규약인지 모르거나 빨강 반전이 의심될 때.

출력 파일과 검증 방법

"측정 확인"은 내려받은 파일을 페이지 밖에서(Pillow·numpy·별도 파서) 다시 열어 그 속성을 측정했다는 뜻입니다. 게임 엔진에서 불러오는 것은 시험하지 않았습니다.

대상파일검증
Channel PNGs<name>-r/g/b/a.png (8-bit grey)측정 확인 numpy채널마다 바이트가 같고 완전히 투명한 텍셀 아래의 RGB도 유지됩니다.
Normal mapPNG · OpenGL (+Y) or DirectX (−Y)측정 확인 numpy디코딩한 벡터가 단위 길이(최대 오차 0.0002)이고, OpenGL과 DirectX는 초록 채널만 다릅니다.
Packed mask / ORMUnity HDRP mask · URP metallic · ORM (glTF, Godot)측정 확인 Pillow패킹된 바이트 확인: ORM 프리셋, AO 10 / 러프니스 80 / 메탈릭 220 → (10, 80, 220, 255).
Engine importUnity · Unreal · Godot미검증어떤 엔진에서도 불러오지 않았습니다.

검증 근거

채널 분리가 네 채널 모두 바이트 동일(최대 차이 0), 투명 텍셀 2048개가 RGB 유지. 가장자리 번짐은 알파 0인 텍셀 448개만 바꾸고 알파 바이트는 하나도 건드리지 않음. OpenGL ↔ DirectX는 초록(255 − g)만 다름. 변환한 노멀은 단위 벡터.

한계

  • 정확한 채널 바이트는 PNG에서만 보장됩니다. JPEG·WebP는 브라우저가 디코딩하며 "바이트 동일 아님"으로 표시합니다.
  • 인터레이스 PNG와 16비트 출력은 지원하지 않습니다. 정확 경로는 3,355만 픽셀까지입니다.
  • 노멀맵의 규약은 픽셀로 알아낼 수 없고 변환만 할 수 있습니다.
  • Unity·Unreal·Godot에서 불러오지 않았습니다. 채널 순서는 각 문서와 엔진 소스에 근거합니다.

버전과 출처

텍스처 랩에서 측정(docs/TEXTURE-LAB.md): 두 다운로드를 채널별로 비교하면 정확히 dx.g == 255 − gl.g, R·B·A는 동일. 변환한 맵은 평균 길이 1.0001, 최대 오차 0.0002로 디코딩됐습니다. 감지 수치는 규약을 아는 실제 맵으로 한 Studio 평가에서 나왔습니다(docs/STUDIO-TEXTURE.md, 2026-09-24). 엔진 규약은 아래 문서에서 인용했으며, 변환한 파일의 엔진 가져오기는 실행하지 않았습니다.

참고한 공식 문서

자주 묻는 질문

내 맵이 어떤 규약인지 알 수 있나요?

아니요. 둘 다 올바른 노멀맵입니다. 랩은 어느 엔진이 무엇을 기대하는지 알려 주고 변환하지만, 감지한다고 주장하지 않습니다.

변환에 손실이 있나요?

없습니다. 초록이 255 − g가 되고, 두 번 변환하면 원래 바이트가 됩니다.

무료인가요? 계정이 필요한가요?

이 페이지의 기능은 모두 무료이며 계정 없이 쓸 수 있습니다. 워터마크나 막혀 있는 내보내기가 없습니다.

스프라이트가 업로드되나요?

아니요. Studio는 이 브라우저 탭 안에서 디코딩·패킹·내보내기를 합니다. 프로젝트는 브라우저(IndexedDB)에 자동 저장되며 .nerulio 파일로 저장할 수 있습니다.

Aseprite나 엔진 플러그인을 설치해야 하나요?

아니요. .aseprite 파일은 브라우저에서 읽고 씁니다. 엔진별 번들에는 그 엔진에 필요한 것이 들어 있습니다(Godot는 가져오기 설정과 씬, Unity와 Godot 타일셋은 작은 편집기 스크립트).