유니티 엔진에서 털을 표현하기 위한 Fur 쉐이더 구현 - Whitmem
유니티 엔진에서 털을 표현하기 위한 Fur 쉐이더 구현
게임 개발 및 엔진
2026-01-14 23:53 게시 4ab030436bb6b2e58811

0
0
42
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
이전에 다른 엔진에서 Fur 쉐이더로 털을 구현한 적이 있다. 이번에는 유니티 엔진에서 털을 구현해보았다. 다만 일반 쉐이더는 Unlit 모드에서 동작되기 때문에, 빛 연산을 사용하기 위해서는 별도 구조체와 함수를 사용해야한다. 유니티 과거 버전에서는 Surface 쉐이더를 지원했기 때문에 Albedo 같은 수치를 직접 넘겨주면 안에서 알아서 처리해줬었는데 워크 프로우가 변경되어서 인지 이제 Surface Shader을 지원하지 않는다고 한다. 즉 직접 라이팅 관련된 작업을 해주거나, 호출해주거나, 쉐이더 그래프를 쓰는 방법이 있다고 한다...
따라서 필자는 쉐이더 그래프를 사용하였는데, 이 게시글에서는 원리만 설명하고 쉐이더 그래프를 배치하는 방법에 대해서는 설명하지 않는다.
이 엔진에서 Fur shader을 어떻게 구현하는지가 주요 관건이지, 쉐이더 그래프를 이리저리 배치해서 Fur 을 구현하는 방법을 알려주는 강좌가 아니기 때문이다.
아무튼 Fur 쉐이더를 구현하면 위와 같은 결과물이 나온다.
매우 깊게 확대하면 위와 같다.
옆면에서 보면 위와 같다. 이걸 보면 알 수 있다시피, 표면을 여러번 렌더링하고 노이즈 맵을 활용하여 구멍을 슝슝 뚫어주면 위에서 볼때 털 처럼되는 것이다.
그렇다고 구멍을 그냥 뚫어버리면, 구멍 뚫린 노이즈 표면이 되는데, 층이 두꺼워질 수록 노이즈의 영향도 더 많이 받도록 하는 것이다.
즉, 층을 0~1 이라는 실수값으로 만들고, 표면에 가까운 value을 0, 제일 높은 층value을 1이라고 했을 때, 현재 value가 노이즈 맵의 noise 보다 큰 경우 잘라낸다.
즉, 노이즈 맵에서 검정(0)인 경우에 해당 지점은 표면 층(정확히는 표면 층은 제외해야 한다. 표면이 구멍 뚫리면 안되기 때문이다.) 부터 끝 층까지 모두 구멍이 뚫리고, 노이즈 맵에서 흰색(1)인 경우에 해당 지점은 표면 층부터, 제일 높은 층 까지 모두 출력된다.
즉 현재 UV 지점의 노이즈가 색상이 위와 같다면, 각 선을 하나의 층(레이어)라고 했을 때, 0~1 비율에 맞춰서 Max, Min 을 색상 값으로 계단식으로 출력하면 되는 것이다.
float nowInstanceRatio = instanceID/maxInstanceCount; if(nowInstanceRatio >= noiseValue){ isClip = 0; }else{ isClip = 1; }
내가 작성한 비교식은 위와 같다. Float형 isClip은 Fragment 노드의 Alpha에 그대로 내보내고, 모드에서 Alpha Clipping 을 활성화 하고, Alpha Clip Threshold는 0.5로 했다. 0이면 출력하지 않고, 1이면 출력하는 것이다.
참고로 표면은 원하는 개수만큼 여러개 출력할 수 있는데, 모두 같은 위치에 출력하는 것이 아니라 레이어 층의 높이 간격을 주어 쌓아야 한다.
이때 쌓는 방향은 단순 평면인 경우 y나 한 방향으로 쌓으면 되지만, 보통 메시에 적용하는 것이기 때문에 노말 방향으로 확장해나가며 쌓으면 된다.
resultPositionOS = positionOS + normalize(normalOS) * moveIntensity * InstanceID;
코드로 보면 위와 같다. InstanceID 에 따라 노말 방향으로 확장해주는 것이다. 10개를 출력하면 10개가 노말 방향으로 확장된다.
참고로 이 작업은 메시를 하나 하나 직접 생성하는 것이 아니라 메시 하나를 렌더링하는 객체에 MonoBehaviour 를 상속받는 클래스를 붙여서 그래픽스 명령을 통해 DrawMeshInstanced를 호출하여 인스턴스 출력을 해 줘야한다.
출력 방법은 예상한대로다. Matrix 정보를 넘겨야하고, Material을 넘겨야하고, Mesh를 넘긴다. Mesh와 Material은 본 메시와 동일하게 가져오기 위해 MeshFilter, MeshRenderer로 부터 가져오고, 매트릭스는 현재 transform과 동일하면 되므로, localToWorldMatrix 를 통해 월드 행렬을 가져와서 동일하게 적용한다.
그리고 출력할 함수는 아래와 같다.
Graphics.DrawMeshInstanced(this.mesh, 0, this.material, matrices, matrices.Length);
이 명령을 수행하면 엔진에서 CommandBuffer의 렌더 시점에 대기열로 등록되어 다음 렌더시에 드로우 된다.
근데 내가 알기론 이거 그래픽을 많이 먹는 작업이 아닌데, 왜이렇게 많이 먹는지 모르겠다. 이건 따로 찾아봐야겠다.
댓글 0개
댓글을 작성하는 경우 댓글 처리 방침에 동의하는 것으로 간주됩니다. 댓글을 작성하면 일회용 인증키가 발급되며, 해당 키를 분실하는 경우 댓글을 제거할 수 없습니다. 댓글을 작성하면 사용자 IP가 영구적으로 기록 및 부분 공개됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.