book/36-tech-face-sdf-and-head-space-control.md

N/A

36. 기술 심화(실전): Face SDF + Head Space Control

얼굴 SDF를 헤드 로컬 축 기준으로 안정화하는 실전 패턴

36. 기술 심화(실전): Face SDF + Head Space Control

[B] 얼굴은 서브컬쳐 렌더링에서 “한 장면의 인상”을 결정하는 영역이라, 일반적인 NdotL 기반 셰이딩만으로는 의도(표정/각도/조명)를 고정하기 어렵습니다. Face SDF(또는 threshold 맵)는 그 부족한 부분을 “데이터”로 보강하는 장치입니다.

  • [B] 목표: 카메라/헤드 회전, 표정 블렌드셰이프, 측광/역광에서도 얼굴 명암 기준이 쉽게 뒤집히지 않게 만들기
  • [B] 핵심 아이디어: 월드축이 아니라 헤드 로컬 축(head forward/right) 을 기준으로 front/side를 계산해 “판정 좌표계”를 캐릭터에 붙인다
  • [B] 구현 포인트: 좌우 UV flip(또는 블렌드)을 통해 텍스처 1장으로 양측 조명 상황을 처리

목적

  • [B] 얼굴 명암 붕괴를 줄이기 위해 Face SDF + 본축 기반 판정을 적용한다.
  • [A/B] URP에서 셰이더와 C# 브릿지를 함께 제시한다.

증거 등급 요약(A/B/C)

  • [B] Face SDF + 좌우 flip 패턴 반복
  • [A] 캐릭터 가독성 우선 설계와 정합
  • [C] 채널 의미는 자산별 상이

핵심 개념

서론: 얼굴이 일반 셰이딩으로 자주 깨지는 이유

  • [B] 얼굴은 미세한 음영 변화가 “표정”으로 읽히기 때문에, 조명 변화에 따라 명암이 뒤집히면 즉시 부자연스럽게 느껴진다.
  • [B] 특히 측광에서 NdotL만으로는 “코/볼/입 주변의 의도”를 유지하기 어렵고, 하드한 램프/라인 시스템과 결합하면 더 쉽게 붕괴한다.

개론: Face SDF(또는 threshold 맵) 입력 계약

  • [B] 필수 입력:
  • [B] FaceSdfTex: 얼굴 음영 경계/우선순위를 담은 텍스처(채널 의미는 팀 규약으로 고정)
  • [B] headForwardWS/headRightWS: 헤드 본축(월드 공간 벡터)
  • [B] L: 라이트 방향(월드)
  • [B] 권장 파라미터:
  • [B] _FaceSmooth: 경계 부드러움
  • [B] _FaceFlipEps: 좌/우 전환 블렌드 구간(하드 flip 팝 방지)

이론: head space 투영으로 “판정 좌표계”를 캐릭터에 붙인다

  • [B] front는 “정면광 정도”를 나타내는 저주파 신호다(보통 head forward와 light를 XZ 평면에 투영해 계산).
  • [B] side는 “좌/우 판정” 신호다(보통 head right와 light의 내적으로 부호를 얻는다).
  • [B] 두 신호를 분리하면 측광에서 경계가 안정되고, 캐릭터가 회전해도 기준이 함께 회전한다.

심화: 하드 flip 대신 ‘블렌드’가 필요한 이유

  • [B] side >= 0 하드 분기는 조명이 정면에 가까울 때 프레임마다 좌/우가 바뀌는 “팝(popping)”이 생긴다.
  • [B] 블렌드는 텍스처 샘플 1회가 추가되지만, 근접 컷의 안정성(temporal) 비용 대비 이득이 크다.

코드 해설(실전)

  • [B] front는 head forward의 XZ 성분만 사용해 고개 숙임/올림에 과민하게 반응하지 않게 만든다.
  • [B] 좌/우 텍스처를 각각 샘플한 뒤, side 기반으로 블렌드해 경계 팝을 줄인다.
  • [C] sdf.x/sdf.y 채널 의미는 자산별로 다를 수 있어 팀 규약 문서가 필수다(예: x=경계 필드, y=보조 임계값).
HLSL
float EvalFaceSdf(float2 uv, float3 L, float3 headForwardWS, float3 headRightWS)
{
    float3 Lxz = normalize(float3(L.x, 0.0, L.z));
    float3 Fxz = normalize(float3(headForwardWS.x, 0.0, headForwardWS.z));
    float front = saturate(dot(Lxz, Fxz) * 0.5 + 0.5);

    float side = dot(normalize(L), normalize(headRightWS));
    float w = smoothstep(-_FaceFlipEps, _FaceFlipEps, side); // 0..1

    float2 uvR = uv;
    float2 uvL = float2(1.0 - uv.x, uv.y);

    float2 sdfR = SAMPLE_TEXTURE2D(_FaceSdfTex, sampler_FaceSdfTex, uvR).ba;
    float2 sdfL = SAMPLE_TEXTURE2D(_FaceSdfTex, sampler_FaceSdfTex, uvL).ba;
    float2 sdf = lerp(sdfL, sdfR, w);

    float a = smoothstep(front - _FaceSmooth, front, sdf.x);
    float b = step(front, sdf.y);
    return a * b;
}
C#
using UnityEngine;

public sealed class FaceAxisBinder : MonoBehaviour
{
    [SerializeField] private Renderer targetRenderer;
    [SerializeField] private Transform headForward;
    [SerializeField] private Transform headRight;
    private static readonly int HeadForwardId = Shader.PropertyToID("_HeadForwardWS");
    private static readonly int HeadRightId = Shader.PropertyToID("_HeadRightWS");
    private MaterialPropertyBlock _mpb;

    private void LateUpdate()
    {
        if (!targetRenderer || !headForward || !headRight) return;
        _mpb ??= new MaterialPropertyBlock();
        targetRenderer.GetPropertyBlock(_mpb);
        _mpb.SetVector(HeadForwardId, headForward.forward);
        _mpb.SetVector(HeadRightId, headRight.right);
        targetRenderer.SetPropertyBlock(_mpb);
    }
}

디버깅 포인트

  • [B] 헤드 본 회전 중 headForward/headRight가 실제 리깅 축과 일치하는지 gizmo로 시각화한다.
  • [B] 좌/우 조명 테스트에서 하드 flip(또는 블렌드) 경계에서 팝이 생기면 _FaceFlipEps를 키워 전환 구간을 넓힌다.
  • [B] 표정 블렌드셰이프 활성 상태에서 SDF 마스크가 입/볼 영역을 과도하게 덮지 않는지 점검한다.

URP 매핑 포인트

설계 해석

  • [B] Face SDF 함수는 일반 Lit 수식과 분리된 include로 관리해야 캐릭터별 튜닝 범위를 통제할 수 있다.

  • [A/B] C# 바인더는 LateUpdate에서 본축을 주입해 애니메이션 갱신 이후 값이 반영되도록 한다.

  • [B] UniversalForward 내 face 분기 함수 적용

  • [B] MaterialPropertyBlock으로 본축 벡터 주입

실패 패턴/오해

  • [B] 월드축 기준 계산으로 회전 시 붕괴
  • [B] 하드 flip(side >= 0)만 사용해 정면광 근처에서 팝(popping) 발생
  • [B] UV 반전/블렌드 누락으로 좌우 광원 불일치
  • [B] Face SDF 텍스처에 sRGB가 켜져 임계값이 드리프트(경계 떨림)

실무 체크리스트

  • 정면광 주변(좌/우 전환 구간)에서 팝이 없는지 _FaceFlipEps

Sources (섹션 단위 인용)