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