Unity URP Shader Study (2) - Shader Graph : 오브젝트에 눈 쌓인 효과 제작

KAY·2023년 6월 11일
0

Study

목록 보기
3/5
post-thumbnail

전체 노드 이미지

예전 프로젝트에서 오브젝트에 눈이 쌓인 효과를 구현해야 했다. Brackeys의 영상을 참고했고, 내 입맛대로 조금씩 수정해서 구현했다.

  • Y축 위로 오브젝트의 버텍스를 조금씩 옮겨 눈이 입체적으로 쌓인 느낌 연출
  • 반짝이는 눈 알맹이 연출 (이거는 게임 Journey의 반짝이는 사막효과처럼 연출해보고 싶었다.)
  • 눈 영역에 투명한 느낌을 주기 위해 Fresnel 효과를 더해줌

반짝이는 알맹이 효과의 레퍼런스는 이거였다. 구현이 반도 안 되긴 했지만...
으악 너무 아름답다. 나는 그냥 Noise로 구현했는데,
사진처럼 구현해내려면 따로 텍스쳐를 쓰는게 좋을 것 같다.
노이즈를 쓰니까 스케일을 키웠을 때, 알갱이들의 패턴이 보여서 아쉬웠다.
예전에 Journey 개발자들의 GDC 영상을 보고 따라해보고 싶었는데 실력이 좀 부족했다.. 다음에 좀 더 도전해보기로.

GDC 영상 링크 : GDC


이제 순서대로 구현을 해보자.
Dot Product등 이해가 안 가는 부분이 너무 많았어서 글이 좀 길어졌다.

1. 눈이 쌓일 방향을 알아보자

일반적으로 눈은 보통 위에서 내리고, 물건의 윗면에 쌓인다. 눈이 쌓일 방향을 알아내기 위해, 월드 노말 벡터와 눈이 쌓일 영역을 Dot 연산한다. 눈은 윗면에 쌓이기 때문에 SnowDir(Vextor3)에는 (0, 1, 0) 값을 넣어주어 Y축 방향대로 1~ -1 값을 가지도록 계산한다.

추가적으로 눈과 오브젝트 텍스쳐간의 경계를 부드럽게 할지, 거칠게 할지를 조절할 수 있게끔 Float형의 SnowSmooth 프로퍼티를 생성해주고, SmoothStep함수에 DotProduct된 값을 in에, SnowSmooth를 Edge2에 넣어준다.

SmoothStep이란?
:Step함수와 비슷한 기능인데, Step은 Edge값 하나만을 받고, Edge값을 기준으로 0과 1만 Out으로 내보낸다.
SmoothStep은 Edge1(최소) Edge2(최대)값을 받고, 두 값 사이를 0~1로 보간한다.

Dot Product란?

벡터 두 개의 값을 알고 있을 때, 두 값이 내적하는 값을 알

Dot Product*가 작동하는 코드는 아래와 같다.

void Unity_DotProduct_float4(float4 A, float4 B, out float Out)
{
Out = dot(A, B);
}

유니티 설명에 따르면 A값에 B 값을 Dot Product (내적 연산) 시킨다는 것인데, 이게 무슨 뜻이냐면..

잠깐 디지털 조명의 원리에 대해 설명을 해보겠다.
조명을 연산할 때 벡터는 매우 중요하다. 오브젝트가 갖고있는 Normal Vector와 조명의 Vector를 연산하여 라이트를 표현하는 것이다.

백터의 내적(Dot)이란, '두 벡터의 각도의 차이를 숫자로 표현한 것' 이다. 내적 이라고 부르며 Dot 이라고 표현한다. A벡터와 B벡터를 닷 연산한다를 표기할 때는 AxB라고 적는다고 한다.

내용 출처 : 테크니컬 아티스트를 위한 유니티 쉐이더 스타트업

내적 값은 벡터가 단위 벡터일 때에 한해서 cos 값과 같다고 한다. 우선 나는 잘 모르겠으니 그냥 외워야한다. cos(x) 는 0도일 때 1, 90도일 때 0, 180도일 때 -1이다.

이미지 출처 : https://www.calculat.org/kr/%EC%82%BC%EA%B0%81%ED%95%A8%EC%88%98/%EC%BD%94%EC%82%AC%EC%9D%B8.html

Sphere 오브젝트가 갖고있는 Normal Vector와 뒤집힌 조명의 Vector를 내적하면 다음과 같은 이미지와 값이 나온다. (왜 뒤집는지 아직 이해를 못했다.유니티에서는 자동으로 뒤집어 준다니까 우선은 그렇게 알자)

이렇게 계산된 값이 들어가면서 대략 아래와 같이 색상이 들어가게 된다.

엔진에서는 0이 블랙이고, -1까지 값은 계산하지 않기 때문에 어두운 면은 완전 어둡게 보인다.
중간 어두운 면이 까맣게만 보이면 안 예쁘니까 dot(s.Normal, LightDir) 한 값에 0.5를 곱하고 0.5를 더하면 간단한 Lambert 라이트가 된다고 한다.

Dot을 설명하다가 너무 길게 와버렸다. 다시 눈 쉐이더로 고고

  • 눈이 오브젝트의 위쪽으로 쌓이게 만들게 하기 위해 World Space에 프로퍼티 눈의 방향(SnowDir)을 Dot 연산 해준다. (조명에서는 Normal Position(Object Space)에 LightDir을 연산해주는 것과 동일)
    아래는 Unlit 쉐이더에 위 Dot 연산한 결과값을 넣어봤을때 보여지는 결과물

    Z방향은 뒤쪽으로 + 라 매우 어둡게 나왔다.

2. 자연스럽게 쌓이는 눈 만들기

월드 포지션의 X, Z를 UV로 갖게끔 만들었는데, 결과물을 보니 굳이
X, Z만 빼오는 귀찮은 과정까지 할 필요는 없는 것 같았다.

눈이 쌓이는 방향도 알아냈고, 해당 방향 값을 텍스쳐에 더해주기만 하면 오브젝트 위 방향으로 하얗게 쌓인 연출은 할 수 있다. 하지만 목표는 자연스러운 눈의 연출이다.
자연스러운 노이즈를 만들기 위해, 월드 포지션을 UV로 갖는 Simple Noise를 생성하고, 스케일을 프로퍼티에서 조절할 수 있도록 Float형의 Snow Scale를 만든다. 이 노이즈를 Remap함수를 이용해 SnowAmount(Range Min-2, Max 0)라는 Float형 프로퍼티를 넣어주어 눈이 쌓인 양을 조절할 수 있도록 한다.

Remap함수 : In MinMax 값을 Out MinMax값으로 비율을 변환하는 함수다.
예를들어, 최소 0, 최대 1을 가질 때, 0.2값을 최소 -1, 최대 1로 Remap 해준다면
결과 값은 -0.6이 된다. In MinMax값을 (0, 1)로 갖고,
Out MinMax 값으로(SnowAmount, 1)을 갖게끔 만들어준다.
SnowAmount의 값만큼 -로 비율 조정을 해, 노이즈가 끼는 양을 조절할 수 있게 된다.

=> 생각해보니까 그냥 subtract (빼기) 해줘도 됐을 것 같다.

위에서 계산한 값을 그대로 쓰면 -값이 나올 수도 있어 오류가 발생할 수 있기 때문에 saturate 함수를 써서 0이하의 값과 1 이상의 값은 없애준다. 그리고 이 노이즈를 1에서 계산한 값에 곱해준다.

이미지와 같은 노드가 된다.


3. 오브젝트의 텍스쳐에 눈 방향 값을 더해주자


디테일한 설정을 하기 위해 눈의 강도를 조절할 수 있게끔 Snow Intensity 프로퍼티를 만들어, Snow Dir과 노이즈 텍스쳐를 곱한 값에 곱해주었다. 차가운 눈 느낌 연출을 위해 씬 화면에서 보면서 값을 넣으려고 색상을 연산해주는 노드를 추가했다. 지금 보니까 오타가 있네. Intensity이다..

Snow Color값까지 받아온 값을 오브젝트 텍스쳐에 Add 해주자. 그러면 텍스쳐 색상 Y축 방향 위로 프로퍼티에서 지정한 색상의 눈이 쌓이는 게 연출 될 것이다. Add 한 Out 값을 마스터 노드의 Albedo값에 넣어주면 우선 색상은 완료이다.


4. 눈의 높이를 계산하자


Snow Intensity를 곱한 값을 가져와 높이 값을 조절할 수 있게 프로퍼티로 만든 Snow Hegiht(Float)를 곱해준다.
오브젝트의 포지션을 기준으로 Y축 위로 눈이 쌓여야 하기 때문에 Object Space의 Position을 가져와, G(Y축)값에 눈 높이를 조절한 값을 더해준다. 이 값들을 다시 Combine해서 마스터 노드의 Position 값에 넣어준다.

여기서 두 가지 문제가 발생했다. Object의 Position 값으로 받아오다보니,
오브젝트의 사이즈에 따라, 오브젝트의 로테이션에 따라 값이 원하는 대로 적용되지 않았다.

1. 사이즈 문제
프로젝트에서 구현해야했던 모델은 Blender에서 Apply > All Transform을 해
유니티에서의 기본 사이즈가 (100, 100, 100)인 오브젝트가 되었다.
그러다보니 높이값을 조절할 때, 눈이 쌓이는 영역이 너무 치솟아버리는
현상이 있어서 0.01을 곱해주었다. 만약, 프로젝트에서 사용해야 하는 오브젝트의 사이즈가 천차만별이라면,
높이 값이 너무 조금이거나, 너무 크게 들어가버리는 문제가 발생할 수 있다.

2. 로테이션 문제
오브젝트 로컬 포지션의 Y축으로 + 해주는 코드이다보니, 오브젝트가 뒤집어졌거나,
많이 기울었을 때, 눈의 높이가 적절하게 적용되지 않은 현상이 있다. 

=> 이 두 가지를 해결하려면 오브젝트가 아닌 월드 포지션을 기준으로 하는 값을
더해주어야 할 것 같다. 이는 추후에 해결해보기로..

5. 눈을 좀 더 예쁘게 만들어보자(글리터 추가, 프레넬 추가)

글리터 추가하기


간단하다. 월드 포지션을 UV로 갖는 Simple Noise 노드를 생성하고, 프로퍼티로 NoiseScale 값을 받아온다. 여기에 얼만큼 빛날 것인지 값을 조절할 수 있게끔 Glitter Intensity(Float)을 곱해주고, 다시 Color값을 곱해준다.

프레넬 추가하기


프레넬은 눈이 쌓이는 영역 말고도 좀 반딱거리는 느낌이 나면 좋을 것 같아 Snow Direction의 Out값을 받아왔는데, 과하다 싶으면 노이즈가 곱해지고 Snow Intensity값이 곱해진 값을 가져와도 괜찮을 것 같다.

스노우 글리터와 프레넬 더해주기

글리터 값과 프레넬 값을 더해주어 이렇게 더한 값을 마스터 노드의 Emission 값에 넣어준다.

6. 노말맵 받기

업로드중..

Normal Texture를 프로퍼티로 받아와서 Sample Texture에 텍스쳐로 넣고, 타입을 노말맵으로 바꿔주고, Normal에 넣어주면 된다.

여러 오브젝트를 넣으면서 좀 깨달은 건데, 텍스쳐와 노말맵이 Snow Height 값에 따라 위로 올라가니까 노말맵과 알베도 맵이 같이 움직여 왜곡되어 보였다. +되는 방향으로, 노말맵 값이 안 보이게 연출하는 쪽이 좋을 것 같은데.. 이건 추후에 연구해보고 넣어볼 예정. 할게 많다!
대략적으로 생각해두면, Snow Height 값을 Normal From Height노드를 이용해 튀어나온 영역의 노말맵을 따로 만들고? Normal Blend에 블랜드 하면 되려나? 생각중


결과물

옛날에 만들었던 인도 풍 조명 모델에 쉐이더 적용한 재질 적용해봤다. 중간에 뚜껑이랑 연결되는 고리를 만들다 말았던 것 같다... 아무튼 이 쉐이더를 잘 변형하면 사막 배경에서 모래 먼지가 쌓인 느낌도 연출할 수 있을 것 같다.

자료 참고
Dot Product : https://tartist.tistory.com/81
Snow Shader Base : Brackeys
Vertex Animation : Unity Shader Training
책 : 테크니컬 아티스트를 위한 유니티 쉐이더 스타트업

0개의 댓글