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

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: 림라이트에 사용할 색상 설정값
릴툰 퍼 셰이더에서는 퍼의 빛 반응을 총 세 가지로 다룸:
- 정면광(diffuse) → 퍼가 빛을 정면으로 받을 때 기본 밝기
- 림라이트(rim) → 퍼의 옆모서리에 생기는 은은한 테두리광
- 조합(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가 나오게 섞어주는 방식임.
퍼 셰이더의 셸마다 이 과정을 반복해서 누적 처리함. 예를 들면:
- 가장 안쪽 셸은 메인 텍스 색에 살짝 얹힘
- 그 위에 퍼가 조금 더 진해짐
- 또 그 위에 퍼가 조금 더 진해짐
- 마지막 가장 바깥쪽 셸에서 가장 밝고 뽀송한 퍼 느낌이 추가됨
릴툰 퍼 셰이더는 퍼를 조금 더 부드럽게 만들기 위해 알파 블렌딩을 활용함.
즉, 퍼가 "완전히 뚜렷하게" 존재하는 게 아니라, 주변으로 점점 투명해지면서 자연스럽게 사라지는 느낌을 줌.
이를 위해 퍼 마스크와 텍스처 알파를 곱하는 것은 기본이고, 추가적으로 레이어 수에 따라 알파를 점진적으로 낮춰줌.
각 레이어마다 퍼의 농도(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 부하 심함. | 레이어 수에 비례한 부하 발생하지만, 기본적으로 훨씬 가벼움. (특히 모바일에서 우세) |
퀄리티 | 고해상도 메시에 대해 매우 자연스러운 곡면 디테일 가능. (특히 파인 퍼, 짧은 털 표현에 유리) | 디테일은 쉘 오프셋 해상도에 의존. 레이어 수가 적으면 퍼 질감이 거칠게 보일 수 있음. |
컨트롤 자유도 | 삼각형 밀도, 위치, 방향성 등 매우 세밀한 조정 가능. | 쉘 개수와 오프셋 양 정도만 조정 가능. (세밀한 털 조작은 한계 존재) |