공부

릴툰(liltoon)의 퍼 쉐이더에 대한 고찰

Myong_ 2025. 4. 29. 16:54

릴 그는 신인가?

 

1. 노멀 방향 기반의 셸 오프셋 계산

 

릴툰 퍼 셰이더는 퍼(fur)를 단일 메시나 단일 표면만으로 표현하지 않음.

마치 실제 털처럼 피부에서 바깥으로 솟아나는 볼륨감을 주기 위해, 쉐이더 안에서 "껍질"을 여러 겹으로 복제해서 표현함.

이 방식이 흔히 말하는 "Shell Technique(쉘 기법)"임.

 

가장 먼저 하는 일은 메시(모델)의 표면이 어느 방향을 향하고 있는지 알아내는 작업.

이걸 노멀(Normal) 방향이라고 부름. 노멀 벡터는 각 버텍스(vertex)마다 존재하며, 표면에서 수직으로 튀어나온 방향을 뜻함

릴툰에서는 이 노멀 벡터를 기준으로 퍼가 자라나는 방향을 계산함.

 

코드 상으로 보면 아래와 같은 계산이 들어감:

float3 normalDirection = normalize(v.normal);

 

v.normal은 버텍스 구조체에 포함된 표면 방향 정보이고, normalize()는 이 벡터의 크기를 1로 정규화해주는 함수.

그래야 방향값으로만 사용 가능함.

 

이 방향 벡터를 기준으로 퍼를 자라나게 하려면, 각 셸(shell)을 조금씩 노멀 방향으로 밀어야 함.

즉, 퍼가 바깥으로 솟아나 있는 것처럼 보여야 하므로, 각 레이어마다 위치를 살짝 이동시킴.

float shellOffset = _FurOffset * layerIndex;
float3 shellPosition = vertexPosition + normalDirection * shellOffset;

 

  • _FurOffset은 퍼의 한 겹 두께 (예: 0.01 정도)
  • layerIndex는 지금 몇 번째 레이어를 처리 중인지 알려주는 인덱스 값
  • vertexPosition은 원래 버텍스의 위치임

이렇게 하면 퍼가 노멀 방향으로 계단처럼 올라간 것처럼 겹겹이 만들어지게 됨.

결과적으로 메시 표면에서 조금씩 바깥으로 부풀어오른 것 같은 여러 층이 만들어지고, 이걸 GPU에서 반복적으로 그려서 마치 진짜 털처럼 보이게 하는 구조임.

 

즉, 퍼 셰이더에서 제일 처음 수행하는 연산은, 각각의 퍼 껍질이 어디 위치해야 자연스러운지 계산하는 작업임.

실제로는 메시의 삼각형이나 버텍스를 더 많이 만드는 건 아니고, GPU 내부에서 버텍스 위치만 임시로 바꿔서 여러 번 그리는 방식이라 퍼포먼스가 좋음. 테셀레이션 같은 고부하 연산이 필요 없고, Unity의 Built-in에서도 잘 동작하는 이유가 여기에 있음.

 


 

2. 셸 반복 구조 (Shell Rendering Structure)

퍼를 한 겹만 그리면 부드럽게 솟은 털 느낌이 전혀 안남.

얇은 이미지 한 장을 피부에서 띄워서 보면 그냥 반투명한 스티커 같음.

그래서 릴툰 퍼 셰이더는 퍼를 여러 겹의 셸(shell)로 나누어 그리는 구조를 채택함.

 

이때 중요한 건 실제로 메시를 복제하지 않는다는 점임.

CPU나 Unity에서 메시를 물리적으로 늘리지 않고, 쉐이더 내에서 GPU가 버텍스를 여러 번 처리하도록 유도하는 방식임.

한마디로 눈속임 기술인데, 이게 엄청 자연스러움.

 

릴툰에서는 이 셸을 최대 8~16겹까지 쌓아올릴 수 있음.

이때 각 셸은 앞서 Step 1에서 설명한 노멀 방향으로 살짝씩 밀려나 있음.

그래서 퍼가 실제로 부풀어 있는 것처럼 보이게 됨.

즉, 한 표면에서 16겹의 털 껍질이 서로 다르게 투명도, 위치, 색상을 가지고 뿌려지게 되는 구조.

 

이를 쉐이더 내부에서는 반복 루프(예: for문)로 처리함:

 


for (int i = 0; i < _FurShellCount; i++)
{
float layerOffset = _FurOffset * (float)i;
float3 shellPos = originalPos + normal * layerOffset;
...
accumulateColor += renderOneLayer(shellPos, i);
}

 

실제로는 각 셸마다 알파(투명도)가 달라야 퍼답게 보임.

안쪽 셸일수록 더 진하고, 바깥쪽 셸일수록 흐릿해야 자연스러움.

그래야 겹쳐졌을 때 점진적으로 사라지는 느낌을 주기 때문.

 

float fade = 1.0 - (i / (_FurShellCount - 1.0));

 

이렇게 해서 fade 값 계산 후 알파에 곱함.

또, 각 셸마다 텍스처 좌표나 노멀, 라이팅을 조금씩 달리해줘야 퍼가 전체적으로 입체감을 가짐.

 

릴툰은 심지어 퍼를 단순히 더하는 게 아니라, 앞쪽 셸의 색과 뒤쪽 셸의 색을 알파 기반으로 부드럽게 보간(lerp) 

모델 자체에는 영향이 없고, 애니메이터나 본 본수에도 영향 없음. 그래서 퍼포먼스가 뛰어나고 안정적인 렌더링이 가능함.

셸 반복 구조의 핵심은 "버텍스를 물리적으로 늘리는 게 아니라 GPU가 위치만 살짝 바꿔가며 여러 번 그리는 것"임.

 

이 구조 덕분에 퍼 셰이더를 그냥 바디 메시 하나만으로도 구현 가능하고, 따로 퍼 메시 만들 필요도 없음...


 

3. 텍스처/마스크 조합에 의한 퍼 형태 조절

 

셸만 겹치기만 하면 퍼처럼 보일 것 같지만, 사실 그렇지 않음.

그냥 투명한 껍질이 겹치기만 하면 퍼의 디테일이나 방향성 같은 느낌이 안 살아남.

그래서 릴툰 퍼 셰이더는 "텍스처"와 "마스크"를 함께 써서 퍼의 모양, 밀도, 방향을 정밀하게 조정함.

 

우선 메인 텍스처를 퍼 텍스처(_FurTex) 사용함.

이 텍스처를 각 셸마다 샘플링해서 퍼의 색깔을 결정함.

float4 furTexColor = tex2D(_FurTex, uv);

 

여기서 uv는 표면상의 텍스처 좌표를 의미함. 모델링할 때 입힌 UV 좌표를 그대로 사용함.

그 다음, 알파 마스크로 퍼 마스크(_FurMask)라는 걸 추가로 사용함.

이건 털이 어디에 있을지, 어디는 없을지를 제어하는 흑백 이미지임. 흰색이면 털이 잔뜩 나고, 검은색이면 털이 아예 없음.

float furAlpha = tex2D(_FurMask, uv).r;

 

furAlpha는 퍼의 "존재 여부"를 조절하는 값이 됨. 이걸 퍼 텍스처의 알파값과 곱해서 최종 알파를 계산함.

finalColor.a = furTexColor.a * furAlpha;

 

결국, 최종적으로 퍼 셰이더에서는 퍼의 색상은 텍스처로, 퍼의 위치와 밀도는 마스크로 관리하는 구조가 됨.

마스크 덕분에 얼굴에는 퍼를 빼고, 몸통에만 퍼를 표현하는 것도 가능해짐.

여기서 중요한 포인트 몇 개 더 있음:

  • 퍼 텍스처와 마스크는 UV에 의존함. 모델링할 때 UV가 깨져있으면 퍼 모양도 이상해짐.
  • 퍼 텍스처 자체가 투명도가 있을 경우(예: 주변이 부드럽게 흐려진 이미지) 퍼의 가장자리가 자연스럽게 사라지는 효과도 만들 수 있음.
  • 마스크 이미지를 노이즈 패턴처럼 만들면 퍼가 군데군데 흩어진 자연스러운 분포를 만들 수 있음.

릴툰 퍼 셰이더의 뛰어난 점은, 이 두 장의 텍스처만으로 퍼의 존재, 형태, 농도, 색깔을 거의 완벽하게 통제할 수 있게 만들어놨다는 거임... 추가적인 퍼 조각 모델링 같은 작업이 필요 없음.

그리고 이 과정은 퍼 셸의 개수만큼 매번 반복됨. 각각의 셸마다 텍스처와 마스크를 다시 적용해서 쌓아가니까, 볼륨이 풍성한 퍼가 구현되는 것임.

 

요약하면:

  • 퍼의 "어디서 자랄지"는 마스크가 결정함.
  • 퍼의 "색깔과 모양"은 퍼 텍스처가 결정함.
  • 둘을 곱해 최종 퍼의 모습과 존재 여부를 만들어냄.

이 구조 덕분에 릴툰 퍼 셰이더는 커스터마이징이 굉장히 쉬움. 텍스처만 바꿔치기하면 다른 종류의 퍼를 금방 만들 수 있기 때문임.

 


 

4. UV 애니메이션에 의한 퍼 움직임 시뮬레이션

 

퍼가 가만히 있으면 부자연스러움. 털이라는 건 원래 바람에 흔들리거나, 캐릭터가 움직일 때 따라 흐느적거려야 함.

그래서 릴툰 퍼 셰이더는 UV 좌표를 시간에 따라 이동시키는 방식으로 퍼가 살아 움직이는 것처럼 시뮬레이션함.

= 퍼를 표현하는 텍스처의 UV를 매 프레임마다 조금씩 밀어주는 것임.

 

마치 이미지가 표면을 따라 슬쩍슬쩍 움직이는 것처럼 만드는 것.

릴툰 퍼 셰이더에서는 이 과정을 이렇게 처리함:

uv += _FurScroll * _Time.y;

 

 

  • _FurScroll: 퍼가 이동할 방향과 속도를 담고 있는 벡터임. (예: (0.1, 0.0)이면 x축 방향으로 천천히 이동)
  • _Time.y: Unity가 자동으로 제공하는 '시간' 값. 게임 실행 후 흐른 시간(초)을 의미함.

 

즉, 시간이 흐를수록 UV 좌표가 계속 밀리게 되고, 퍼 텍스처도 살짝살짝 이동하는 것처럼 보이게 됨.

퍼가 바람에 흔들리듯 자연스럽게 움직이는 효과가 나옴.

 

이 방식은 진짜 모델의 버텍스를 움직이거나 본(bone) 애니메이션을 추가하는 게 아님.

그냥 텍스처만 살짝 이동시켜서 퍼가 부드럽게 흩날리는 것처럼 착시를 일으키는 것임... 그래서 퍼포먼스 부담이 거의 없음.

 

추가로 릴툰 퍼 셰이더는 셸마다 UV를 살짝씩 더 이동시켜줌. 레이어별로 이동량을 다르게 주는 식임:

uv += layerIndex * _FurLayerScroll;
  • layerIndex: 몇 번째 셸인지
  • _FurLayerScroll: 한 레이어당 추가로 이동시킬 양

이렇게 하면 바깥쪽 퍼일수록 더 많이 흔들리고, 안쪽 퍼는 덜 흔들리게 설정할 수 있음.

실제 털이 깊이에 따라 바람의 영향을 다르게 받는 것처럼 보이게 하는 디테일임.

 

요약하면,

  • 퍼 텍스처의 UV를 시간에 따라 조금씩 이동시켜 퍼가 흔들리는 듯한 효과를 만듦
  • 셸마다 이동량을 다르게 해서 입체감을 살림
  • 계산량은 매우 적음 (단순 UV 이동 연산)

덕분에 퍼가 정적인 느낌 없이 계속 살아 있는 것처럼 보임. 간단하지만 굉장히 효율적인 방법임.

 


 

 

5. 빛 반응 및 림라이트 계산

퍼를 표현할 때 가장 중요한 것 중 하나는 빛 반응임.

 

빛을 정면에서 받을 때와 옆에서 받을 때 색이나 밝기가 달라져야 현실적으로 보임.

래서 릴툰 퍼 셰이더는 노멀 방향과 빛 방향을 비교해서 퍼가 빛을 어떻게 반사할지 계산함.

가장 기본은 Lambert 방식. 노멀 벡터와 광원 방향 벡터의 내적(dot product)을 이용해서 빛의 세기를 계산함:

float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float ndotl = saturate(dot(normalDirection, lightDir));

 

 

  • lightDir은 월드 기준 광원 방향
  • normalDirection은 버텍스가 향하고 있는 방향
  • dot(normal, lightDir)은 두 벡터의 방향이 얼마나 일치하는지 나타냄 (1이면 완전히 같은 방향, 0이면 수직)

 

saturate()를 써서 결과를 0~1로 고정함. 그래야 계산 안정성 확보 가능.

ndotl 값이 높으면, 즉 표면이 빛 쪽을 향하고 있으면 퍼가 밝게 보이고, 낮으면 어둡게 됨. 이게 기본적인 퍼의 밝기 연출임.

 

 

퍼 특유의 가장자리 반짝임은 림라이트(Rim Light) 효과로 구현함.

퍼는 표면 옆면 쪽에서 빛을 받으면 윤곽선이 반짝이는 특징이 있음.

이걸 셰이더로 계산하려면 뷰 방향(view direction)과 노멀 방향을 비교하면 됨.

 

뷰 방향은 카메라에서 버텍스까지의 방향임:

float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos);

 

그 다음, 노멀과 뷰 방향의 내적을 이용해 림 값을 만듦:

float rim = pow(1.0 - saturate(dot(viewDir, normalDirection)), _FurRimPower);

 

  • dot(viewDir, normalDirection)은 1에 가까울수록 카메라를 향하고, 0에 가까울수록 옆을 향함
  • 1.0 - dot()을 하면 옆으로 갈수록 값이 커짐
  • pow(..., _FurRimPower)로 부드럽거나 뾰족한 림을 조절 가능

이렇게 구한 rim 값은 퍼 셰이더에서 따로 색깔이나 밝기 보정을 넣을 때 사용함. 일반적으로는 살짝 밝게 해주는 정도로 처리함:

finalColor.rgb += rim * _FurRimColor.rgb;

 

  • _FurRimColor: 림라이트에 사용할 색상 설정값

 

 

릴툰 퍼 셰이더에서는 퍼의 빛 반응을 총 세 가지로 다룸:

  1. 정면광(diffuse) → 퍼가 빛을 정면으로 받을 때 기본 밝기
  2. 림라이트(rim) → 퍼의 옆모서리에 생기는 은은한 테두리광
  3. 조합(fresnel 효과 비슷) → 둘을 적절히 섞어 입체감 강화

이 과정을 통해 퍼가 평평하게 보이지 않고, 실제 털처럼 볼륨감 있게 표현됨.

특히 릴툰 퍼 셰이더는 림라이트를 꼭 흰색으로 하지 않고, 사용자가 원하는 컬러를 입힐 수 있게 해놨음.

또, 셸마다 이 빛 반응 계산을 반복함. 즉, 각각의 퍼 레이어가 따로따로 빛을 받아서, 바깥쪽 셸은 림 효과가 더 강하고, 안쪽 셸은 덜한 느낌으로 자연스럽게 섞이는 구조임. 퍼가 "단일 덩어리"가 아니라 "털 한 올 한 올"처럼 느껴지는 이유가 여기에 있음.

 


 

6. 최종 색상 혼합 (Final Color Blending)

 

여기까지 퍼의 위치, 텍스처, 마스크, UV 이동, 빛 반응, 림라이트까지 다 준비했으면, 마지막으로 해야 할 일은 이 모든 요소를 하나로 합쳐서 최종 색상을 만드는 것임. 릴툰 퍼 셰이더는 이 최종 단계에서 퍼가 피부와 자연스럽게 섞이도록 만들어줌.

 

float4 skinColor = SampleSkinTexture(uv);
float4 furColor = SampleFurTextureAndLighting(uv, layerIndex);
finalColor = lerp(skinColor, furColor, furAlpha);

 

  • skinColor: 퍼가 없는 경우 보여줄 기본 피부 색상
  • furColor: 지금 계산한 퍼 색상 (조명, 림 효과 반영된 값)
  • furAlpha: 퍼의 존재 여부를 나타내는 알파 값 (0이면 퍼 없음, 1이면 퍼 100%)

lerp(a, b, t)는 a와 b 사이를 t값만큼 보간하는 함수임.

쉽게 말하면 t=0이면 a, t=1이면 b, 중간값이면 그 중간 색깔을 만드는 함수임.

 

즉, 퍼가 진한 곳은 furColor가 완전히 덮고, 퍼가 희미한 곳이나 없는 곳은 skinColor가 나오게 섞어주는 방식임.

 

퍼 셰이더의 셸마다 이 과정을 반복해서 누적 처리함. 예를 들면:

  1. 가장 안쪽 셸은 메인 텍스 색에 살짝 얹힘
  2. 그 위에 퍼가 조금 더 진해짐
  3. 또 그 위에 퍼가 조금 더 진해짐
  4. 마지막 가장 바깥쪽 셸에서 가장 밝고 뽀송한 퍼 느낌이 추가됨


릴툰 퍼 셰이더는 퍼를 조금 더 부드럽게 만들기 위해 알파 블렌딩을 활용함.

즉, 퍼가 "완전히 뚜렷하게" 존재하는 게 아니라, 주변으로 점점 투명해지면서 자연스럽게 사라지는 느낌을 줌.

이를 위해 퍼 마스크와 텍스처 알파를 곱하는 것은 기본이고, 추가적으로 레이어 수에 따라 알파를 점진적으로 낮춰줌.

각 레이어마다 퍼의 농도(furAlpha)가 다르고, 빛 반응(rim)도 다름. 그래서 겹치면서 입체감이 쌓이는 구조임.

 

이렇게 반복적으로 누적해나가면서 최종적으로 부드럽고 풍성한 퍼가 만들어지는 것.

 

 

 


 

퍼 쉐이더쪽 개발하려고 아플라님이랑 얘기 나누면서 당연히 릴툰이 테셀레이션 썼을거라고 생각했는데

아닌 것 같다고 하셔가지고 기억의 저편에서 예전에 코드 분석 했던 것을 꺼내와서 다시 정리해봤음...

 

요즘 매일매일 릴에 대한 경외심만 늘어가고 있음...

 

 

아래는 GPT가 정리해준 테셀레이션 vs 쉘 오프셋 중첩 방식 쉐이더의 비교표.

 

항목 테셀레이션 다중 레이어 (쉘 오프셋)
구조 메시(삼각형)를 셰이더 단계에서 실시간으로 더 잘게 쪼갬. 기존 메시를 복제하지 않고, 버텍스 위치를 쉘처럼 여러 번 오프셋 이동해서 겹침.
GPU 처리 단계 Hull Shader + Domain Shader 추가 필요.
(Tessellation Stage)
Vertex Shader와 Fragment Shader만 사용.
(전통적인 렌더링 파이프라인)
버텍스 수 증가 방식 실시간으로 삼각형을 추가 생성 → 버텍스 수 급증. 버텍스는 증가하지 않고, 쉘을 오프셋 이동하면서 레이어별로 다시 그려서 표현.
필요한 하드웨어 Shader Model 4.6 이상, 테셀레이션 지원 GPU 필수. Shader Model 3.0 이상, 대부분의 GPU에서 동작 가능.
메모리 사용량 메시 데이터가 동적으로 증가 → VRAM 사용량 증가 가능성 있음. 버텍스 복제 없이 렌더링만 반복 → 메모리 부담 거의 없음.
연산 부하 테셀레이션 계산 + 추가 버텍스 처리 비용 존재. 레이어 수만큼 쉐이더 파이프라인 반복 → 약간의 추가 부하는 있음.
성능 고정된 폴리곤 수 상황에서는 효율적. 하지만 실시간 변형시 GPU 부하 심함. 레이어 수에 비례한 부하 발생하지만, 기본적으로 훨씬 가벼움. (특히 모바일에서 우세)
퀄리티 고해상도 메시에 대해 매우 자연스러운 곡면 디테일 가능. (특히 파인 퍼, 짧은 털 표현에 유리) 디테일은 쉘 오프셋 해상도에 의존. 레이어 수가 적으면 퍼 질감이 거칠게 보일 수 있음.
컨트롤 자유도 삼각형 밀도, 위치, 방향성 등 매우 세밀한 조정 가능. 쉘 개수와 오프셋 양 정도만 조정 가능. (세밀한 털 조작은 한계 존재)