헤어 셰이더 Anisotropy Direction Map 오류를 자동 검출하는 탄젠트 방향 시각화 툴

헤어 셰이더의 Anisotropy Direction Map에서 반전, 빈 픽셀, UV 심, 불연속 오류를 자동 검출하는 탄젠트 방향 시각화 원리와 Unity URP용 구현 방법을 정리한다.

✓ 복사 완료!
챕터셰이더 프로그래밍 (HLSL/ShaderGraph)
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
헤어 셰이더 Anisotropy Direction Map 오류를 자동 검출하는 탄젠트 방향 시각화 툴

핵심 요약

헤어 셰이더의 Anisotropy Direction Map 오류는 RGB 텍스처를 그대로 확인하는 것만으로 찾기 어렵다. 텍셀의 방향 벡터를 복원한 뒤 길이, 각도, 이웃 텍셀과의 불연속, 예상 탄젠트 방향과의 편차를 계산하면 반전·빈 픽셀·UV 심 오류를 자동 표시할 수 있다.

이 글에서는 Unity 2022.3 LTS와 URP의 HLSL을 기준으로 R=X, G=Y, B=유효성 마스크인 방향 맵을 사용한다. 디버그 셰이더는 방향 각도를 색상으로 표시하고 오류 픽셀은 빨간색으로 강조한다.

Anisotropy Direction Map은 무엇을 저장하는가?

헤어 셰이더의 Anisotropy는 빛의 하이라이트가 머리카락의 흐름을 따라 길게 늘어나는 현상이다. 이때 셰이더는 표면의 기본 탄젠트 또는 헤어 카드에 저장된 방향 정보를 사용해 스펙큘러 하이라이트의 방향을 결정한다.

Direction Map은 일반적인 컬러 텍스처가 아니라 방향 벡터를 인코딩한 데이터 텍스처다. 이 글에서 사용할 인코딩 규칙은 다음과 같다.

채널의미복원 방법
R탄젠트 공간 X 방향R * 2 - 1
G탄젠트 공간 Y 방향G * 2 - 1
B방향 데이터 유효성B
A선택적 강도 또는 블렌드 값프로젝트 규칙에 따름

2D 방향 벡터는 다음과 같이 복원한다.

Dmap=normalize(float2(R,G)∗2−1)D_{map} = normalize(float2(R, G) * 2 - 1)

여기서 복원된 벡터의 길이가 지나치게 짧으면 방향을 결정할 수 없다. 따라서 다음 조건을 별도로 검사해야 한다.

  • length(RG * 2 - 1) < 0.25: 방향 벡터가 거의 없음
  • B < 0.5: 유효하지 않은 픽셀
  • 인접 픽셀과의 각도 차이가 임계값보다 큼: 급격한 불연속
  • 예상 헤어 흐름과의 내적이 음수: 방향 반전 가능성

프로젝트에 따라 R/G의 기준축이 달라질 수 있다. 예를 들어 V+ 방향을 머리카락 뿌리에서 끝 방향으로 사용할 수도 있고 V- 방향을 사용할 수도 있다. 이 규칙을 확정하지 않으면 정상적인 방향 반전도 오류로 판정할 수 있다.

헤어 카드에 저장된 방향 맵과 복원된 탄젠트 벡터를 비교하는 예시

왜 방향 맵 오류는 최종 라이팅에서 늦게 발견되는가?

Direction Map은 텍스처 자체만 보면 색상 그라데이션처럼 보인다. 하지만 헤어 셰이더는 이 값을 벡터로 복원해 하이라이트 방향과 스펙큘러 응답에 사용한다. 다음 오류는 일반적인 텍스처 뷰어에서 쉽게 놓친다.

오류 유형텍스처에서 보이는 모습실제 셰이더 결과
R/G 채널 반전색상 변화가 자연스러움하이라이트가 반대 방향으로 흐름
빈 방향 픽셀검정 또는 회색 영역하이라이트가 끊기거나 무작위로 이동
UV 심 불일치양쪽 색상이 비슷해 보임카드 경계에서 밝은 선 또는 단절 발생
과도한 압축작은 색상 차이스트랜드가 얼룩처럼 반짝임
방향 벡터 길이 부족중간색에 가까운 영역노멀과 탄젠트 계산이 불안정함
좌우 미러링 오류대칭적으로 정상처럼 보임한쪽 카드만 반대 방향으로 빛남

특히 헤어 카드가 좌우 대칭 UV를 사용하면 미러링된 카드의 탄젠트 방향이 뒤집힐 수 있다. 이 문제는 정면에서는 잘 보이지 않고 특정 각도의 키 라이트나 림 라이트에서만 드러나는 경우가 많다.

탄젠트 방향을 색상과 선으로 시각화하는 원리는 무엇인가?

디버그 뷰는 방향 벡터를 세 가지 방식으로 동시에 보여주는 것이 좋다.

  1. 방향 각도를 HSV 색상으로 변환한다.
  2. 방향 벡터의 길이와 유효성 마스크를 밝기로 사용한다.
  3. 예상 방향과의 편차 또는 이웃 방향과의 불연속을 빨간색으로 덮어쓴다.

2D 방향 벡터의 각도는 다음과 같이 계산할 수 있다.

θ=atan2(Dy,Dx)\theta = atan2(D_y, D_x)

각도를 0에서 1 사이로 정규화하면 색상환에 매핑할 수 있다.

H=θ+π2πH = \frac{\theta + \pi}{2\pi}

예상 방향 벡터를 D_expected라고 할 때 두 방향의 유사도는 내적으로 계산한다.

Sdirection=dot(Dmap,Dexpected)S_{direction} = dot(D_{map}, D_{expected})

S_direction이 -1에 가까우면 두 벡터가 정반대라는 뜻이다. 다만 헤어의 방향성은 양방향 벡터로 취급하는 셰이더도 있으므로 프로젝트 규칙에 따라 abs(dot(...))를 사용할지 결정해야 한다.

이웃 텍셀 간의 급격한 변화는 다음 방식으로 찾을 수 있다.

Eneighbor=1−dot(Dcenter,Dneighbor)E_{neighbor} = 1 - dot(D_{center}, D_{neighbor})

E_neighbor가 크면 방향이 급격히 바뀐 것이다. 헤어카드의 의도적인 컬이나 갈라짐도 불연속으로 나타날 수 있으므로 오류 임계값과 허용 영역을 함께 제공해야 한다.

자동 오류 검출은 어떤 순서로 구현할까?

Step 1. 방향 맵의 인코딩 규칙을 고정한다

먼저 텍스처의 색 공간과 채널 규칙을 고정한다.

  • Direction Map은 sRGB가 아닌 Linear 데이터로 임포트한다.
  • R/G는 0.5를 중립 방향으로 사용하는 signed encoding으로 정의한다.
  • B는 유효성 마스크로 사용하거나 사용하지 않을 경우 항상 1로 채운다.
  • 압축 포맷은 프로젝트 플랫폼에서 R/G 정밀도를 보존하는 설정을 사용한다.
  • 좌우 미러 UV에서 방향을 반전할지 여부를 아트 툴과 셰이더 양쪽에 문서화한다.

sRGB로 임포트하면 0.5가 선형 공간의 0.5로 유지되지 않기 때문에 방향 벡터가 왜곡될 수 있다. 이 문제는 중립 방향 주변의 미세한 하이라이트 떨림으로 나타난다.

Step 2. 유효성, 길이, 방향 반전을 계산한다

다음은 Unity URP 2022.3 LTS의 HLSL 디버그 셰이더에서 사용할 수 있는 핵심 함수 예시다.

struct DirectionDebugResult
{
    float2 direction;
    float lengthValue;
    float valid;
    float reverseError;
};

DirectionDebugResult DecodeDirection(float4 mapSample, float2 expectedDirection)
{
    DirectionDebugResult result;

    float2 encoded = mapSample.rg * 2.0 - 1.0;
    result.lengthValue = length(encoded);
    result.valid = step(0.25, result.lengthValue) * step(0.5, mapSample.b);

    result.direction = result.lengthValue > 0.0001
        ? encoded / result.lengthValue
        : float2(0.0, 1.0);

    float similarity = dot(result.direction, normalize(expectedDirection));
    result.reverseError = step(similarity, -0.5) * result.valid;

    return result;
}

expectedDirection은 단순히 (0, 1)로 고정할 수도 있지만 실제 헤어 카드의 UV 흐름이나 아티스트가 지정한 기준 방향에서 계산하는 편이 안전하다.

Step 3. 이웃 텍셀과 비교해 불연속을 찾는다

방향 맵 텍스처의 인접 샘플을 읽어 방향 변화량을 계산한다. 텍스처 크기와 밉맵 단계에 따라 샘플 오프셋이 달라지므로 디버그 뷰에서는 텍셀 크기를 명시적으로 전달하는 것이 좋다.

float DirectionDiscontinuity(
    Texture2D directionMap,
    SamplerState linearClampSampler,
    float2 uv,
    float2 texelSize,
    float2 centerDirection)
{
    float2 uvRight = uv + float2(texelSize.x, 0.0);
    float2 uvUp = uv + float2(0.0, texelSize.y);

    float2 rightEncoded = directionMap.SampleLevel(
        linearClampSampler, uvRight, 0).rg * 2.0 - 1.0;
    float2 upEncoded = directionMap.SampleLevel(
        linearClampSampler, uvUp, 0).rg * 2.0 - 1.0;

    float2 rightDirection = normalize(rightEncoded + float2(0.0001, 0.0001));
    float2 upDirection = normalize(upEncoded + float2(0.0001, 0.0001));

    float rightError = 1.0 - dot(centerDirection, rightDirection);
    float upError = 1.0 - dot(centerDirection, upDirection);

    return max(rightError, upError);
}

실제 구현에서는 텍스처 경계와 타일링을 고려해야 한다. Clamp 샘플러로 경계를 읽으면 카드 가장자리에서 불필요한 오류가 발생할 수 있으므로 UV 영역 안쪽만 검사하거나 경계 픽셀의 가중치를 낮춘다.

방향 벡터의 각도와 불연속 오류를 색상으로 표시한 디버그 뷰

디버그 셰이더는 어떻게 색상을 구성할까?

방향을 직관적으로 확인하려면 각도를 색상환에 매핑한다. 다음 예시는 HSV의 Hue만 사용하는 간단한 RGB 변환 함수다.

float3 HsvToRgb(float3 hsv)
{
    float3 rgb = abs(frac(hsv.xxx + float3(0.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0);
    rgb = saturate(rgb - 1.0);
    return hsv.z * lerp(float3(1.0, 1.0, 1.0), rgb, hsv.y);
}

float3 DirectionToColor(float2 direction, float strength)
{
    float angle = atan2(direction.y, direction.x);
    float hue = frac((angle + 3.14159265) / 6.28318530);
    return HsvToRgb(float3(hue, 0.85, saturate(strength)));
}

프래그먼트 단계에서는 다음 우선순위로 오류 색상을 결정한다.

float3 BuildDebugColor(
    float4 directionSample,
    float2 expectedDirection,
    float discontinuity)
{
    DirectionDebugResult decoded = DecodeDirection(
        directionSample,
        expectedDirection);

    float3 directionColor = DirectionToColor(
        decoded.direction,
        decoded.lengthValue);

    float invalidError = 1.0 - decoded.valid;
    float discontinuityError = step(0.65, discontinuity);
    float reverseError = decoded.reverseError;

    float3 color = directionColor;
    color = lerp(color, float3(1.0, 0.0, 0.0), invalidError);
    color = lerp(color, float3(1.0, 0.35, 0.0), discontinuityError);
    color = lerp(color, float3(0.8, 0.0, 1.0), reverseError);

    return color;
}

권장 색상 규칙은 다음과 같다.

표시 색상의미
색상환정상적인 탄젠트 방향
빨간색유효성 마스크 누락 또는 벡터 길이 부족
주황색이웃 텍셀과 방향이 급격히 다름
보라색예상 방향과 반대
어두운 색방향 벡터 강도가 낮음

오류 표시를 단순히 빨간색 하나로 통합하면 원인을 구분하기 어렵다. 검수 단계에서는 오류 유형별 색상을 유지하고 최종 제출용 뷰에서만 단일 경고 색상으로 합치는 편이 좋다.

탄젠트 방향 시각화 툴의 전체 처리 흐름

flowchart LR
    A[Direction Map 입력] --> B[Linear 샘플링]
    B --> C[RG signed vector 복원]
    C --> D[길이와 유효성 검사]
    C --> E[예상 방향과 내적 비교]
    C --> F[이웃 텍셀 불연속 검사]
    D --> G[오류 마스크 결합]
    E --> G
    F --> G
    G --> H[각도 색상화]
    G --> I[오류 오버레이]
    H --> J[디버그 뷰 출력]
    I --> J

이 구조를 에디터 툴로 확장하면 텍스처를 화면에서 확인하는 수준을 넘어 자동 검수 리포트까지 만들 수 있다. 예를 들어 전체 픽셀 중 오류 비율, 가장 큰 불연속 위치, UV 섬별 평균 방향, 반전 의심 영역을 CSV 또는 JSON으로 저장할 수 있다.

UV 심과 미러 카드 오류는 어떻게 구분할까?

모든 방향 불연속이 제작 오류는 아니다. 헤어카드는 다음과 같은 이유로 의도적인 방향 변화가 생길 수 있다.

  • 앞머리와 옆머리의 흐름이 달라지는 경우
  • 컬이나 웨이브가 시작되는 구간
  • UV 섬이 분리되어 실제 3D 공간에서 방향이 바뀌는 경우
  • 좌우 대칭 카드에서 탄젠트 기준을 반전한 경우

따라서 자동 검출기는 오류 점수만 출력하지 말고 원인 후보를 함께 제공해야 한다.

검사권장 판정 기준
벡터 길이length < 0.25이면 오류 후보
유효성 마스크B < 0.5이면 오류 후보
이웃 각도50~70도 이상이면 불연속 후보
기준 방향 내적dot < -0.5이면 반전 후보
오류 픽셀 비율전체의 1% 이상이면 검수 경고
UV 심 주변심 양쪽의 방향 차이를 별도 집계

컬처럼 방향이 빠르게 변하는 영역은 아티스트가 허용 마스크를 지정할 수 있게 한다. 허용 마스크가 있는 경우 최종 오류 점수는 다음과 같이 계산할 수 있다.

Efinal=Einvalid∨((1−Mallow)∧Ediscontinuity)E_{final} = E_{invalid} \lor ((1 - M_{allow}) \land E_{discontinuity})

M_allow가 1인 영역에서는 의도적인 불연속을 검사 대상에서 제외한다.

제작 파이프라인에 자동 검출을 어떻게 연결할까?

1. 임포트 단계에서 텍스처 설정을 검사한다

Unity AssetPostprocessor 또는 사내 검수 스크립트에서 다음 항목을 확인한다.

  • 텍스처가 sRGB로 설정되어 있지 않은가?
  • R/G 채널이 제거되는 압축 포맷은 아닌가?
  • 밉맵 생성이 필요한 카드에 밉맵이 누락되지 않았는가?
  • 알파 채널의 의미가 프로젝트 규칙과 일치하는가?
  • 텍스처 이름과 머티리얼 연결이 올바른가?

2. 에디터 뷰에서 방향과 오류를 동시에 확인한다

검수자는 다음 세 가지 모드를 빠르게 전환할 수 있어야 한다.

  1. Direction: 각도 색상만 표시
  2. Validity: 길이와 마스크 상태만 표시
  3. Error: 반전과 불연속을 오류 색상으로 표시

가능하면 마우스 위치의 UV, 복원된 방향 벡터, 예상 방향과의 내적, 오류 유형을 툴팁으로 제공한다. 숫자 정보를 함께 보여주면 색상만으로 판단할 때 발생하는 오해를 줄일 수 있다.

3. 빌드 전 자동 검수 결과를 저장한다

CI 또는 에셋 검수 단계에서는 전체 텍스처를 분석해 다음 값을 기록한다.

{
  "asset": "hair_front_direction.tga",
  "resolution": "2048x2048",
  "invalid_ratio": 0.0021,
  "reverse_ratio": 0.014,
  "discontinuity_ratio": 0.008,
  "max_neighbor_error": 0.91,
  "status": "warning"
}

예를 들어 invalid_ratio > 0.01 또는 reverse_ratio > 0.02인 경우 경고를 발생시키도록 설정할 수 있다. 임계값은 프로젝트의 카드 수, 텍스처 해상도, 컬 표현 방식에 맞춰 조정해야 한다.

런타임 비용은 어떻게 관리할까?

이 시각화 기능은 최종 머티리얼에 항상 포함할 필요가 없다. 다음 방식으로 분리하는 것이 안전하다.

  • DEBUG_HAIR_DIRECTION 셰이더 키워드로 디버그 코드를 조건부 컴파일한다.
  • 이웃 샘플 검사는 에디터와 개발 빌드에서만 활성화한다.
  • 방향 색상 뷰와 오류 검출 뷰를 별도의 패스로 분리한다.
  • 모바일에서는 방향 색상만 제공하고 이웃 샘플 검사는 오프라인 툴에서 수행한다.
  • 실제 라이팅 결과와 방향 디버그 결과를 화면 분할로 비교한다.

이웃 텍셀 검사에는 추가 텍스처 샘플이 필요하다. 따라서 런타임 성능이 중요한 플랫폼에서는 GPU 디버그 패스보다 CPU 기반 오프라인 분석기를 사용하는 편이 효율적이다.

자주 묻는 질문 (FAQ)

Direction Map은 반드시 sRGB를 꺼야 하나?

그렇다. R/G가 방향 벡터 데이터라면 일반적으로 Linear 데이터로 읽어야 한다. sRGB 보정은 채널 값을 비선형으로 바꾸어 복원된 방향을 왜곡한다.

모든 방향 반전을 오류로 판정해도 되나?

아니다. 셰이더가 방향을 양방향 벡터로 처리하거나 미러 UV에서 탄젠트를 의도적으로 반전하는 경우가 있다. 먼저 프로젝트의 기준축과 미러링 규칙을 정의한 뒤 기준 방향과 비교해야 한다.

방향 맵의 RGB 색상만 보면 오류를 찾을 수 없나?

대부분 어렵다. 색상은 벡터 길이, 유효성, UV 심, 인접 방향 관계를 직관적으로 보장하지 않는다. 방향 벡터를 복원하고 수치 검사를 수행한 뒤 색상과 오류 오버레이로 시각화해야 한다.

정리

헤어 셰이더의 Anisotropy Direction Map은 색상 텍스처가 아니라 방향 데이터다. 따라서 검수 툴도 RGB 이미지를 단순히 보여주는 방식에서 벗어나 RG → signed direction vector 복원, 벡터 길이 검사, 기준 방향과의 내적 비교, 이웃 텍셀 불연속 검사를 수행해야 한다.

가장 실용적인 구성은 방향 각도를 색상환으로 표시하고 유효성 오류는 빨간색, 불연속은 주황색, 반전은 보라색으로 분리하는 것이다. 여기에 임포트 설정 검사와 CI용 오류 비율 리포트를 연결하면 헤어카드 제작 오류를 라이팅 단계보다 훨씬 앞에서 발견할 수 있다.