베개발

엔진심화 10. 아웃라인 셰이더 본문

대학교 공부

엔진심화 10. 아웃라인 셰이더

rusal0204 2021. 6. 9. 23:59

목차

  • 맥스에서 외곽선 만들어보기 (아웃라인 원리 이해하기)
  • 엔진에서 만들어보기
  • BRDF
  • 프레넬

* 공부 과정에서 작성한 글이기 때문에 틀린 내용이 있을 수 있습니다.


■맥스에서 외곽선 만들어보기 (아웃라인 원리 이해하기)

예전에 만들었던 개인작

위 작업은 맥스에서 렌더링한것이다. 잘 보면 파란색으로 외곽선들이 있는 것을 볼 수가 있다.

외곽선이 있는 것과 없는 것을 비교해보면 다음과 같다.

 

외곽선 셰이더를 만들어보기 전에 우선 맥스에서 어떤 원리로 외곽선을 표현할 수 있을지 알아보자.

 

왼쪽부터 차례로 최종본, 원본 메쉬, 외곽선 메쉬이다.

최종본은 원본 메쉬와 외곽선 메쉬를 같은 자리에 합쳐 두어서 완성된 모습이다.

 

외곽선 메쉬에는 다음과 같은 모디파이어들이 들어가있다.

여기서 먼저 Normal 모디파이어는 오브젝트의 노말, 즉 면이 바라보는 방향을 완전히 뒤집어준다.

왼쪽부터 차례로 원본 / 노말 모디파이어 적용

위 이미지를 보면 노말을 뒤집었을 때 빛의 방향이 뒤집어지는 것을 확인할 수 있다.

맥스에서는 노말 모디파이어를 적용하기 전에, 오브젝트에 Backface Cull 옵션을 적용해줘야 뒤집힌 면으로 제대로 보인다.

 

그 다음 Push 모디파이어는 오브젝트의 버텍스가 각각 정방향으로 이동하게 해준다.

*위 이미지에서는 노말이 뒤집혀있기 때문에, Push 값이 작아질수록 밖으로 부푸는 것처럼 보인다.

 

만약, 하드엣지 (스무딩그룹이 나뉘어진 상태의 폴리곤들)가 있는 상태로 엔진에 가져가서 push 모디파이어와 같은 식으로 셰이더를 적용해주게 된다면, 아래 사진처럼 분리가 되는 듯이 나온다. 

 

엔진에 가져갔을 때에는 스무딩그룹이 나뉜 면들끼리는 분리된 것과 같은 상태로 넘어가기 때문이다. (즉, 하드엣지가 있는 부분에는 버텍스가 두개씩 겹쳐있다고 보면 된다. 하나의 버텍스에 노말 방향이 한번에 두 개가 될 수 없기 때문에)

위 사진은 하드엣지가 들어간 정육면체 오브젝트를 엔진으로 가져가서, 간단한 외곽선 셰이더를 적용시켜본 모습이다.

 

 

이렇게 외곽선의 원리를 알아봤으니 이제 엔진에서 직접 만들어보자.

 

 

■유니티 URP에서 외곽선 만들어보기

 

유니티에서 아웃라인을 사용하기 위해서, 먼저 Forward Renderer에 추가 패스를 생성해야한다.

Add Renderer Feature에서 Render Objects를 추가하고,

Filters > LightMode Tags에 Outline을 추가해준다.

 

이때 바로 그려지지 않는데, 여기에서 레이어 마스크를 Everything으로 해줘야 렌더되기 시작한다.

만약 외곽선 셰이더를 지정하고자 하는 오브젝트의 레이어가 'Character'이라면, 해당 레이어 마스크를 Character로 설정함으로써 캐릭터 레이어의 오브젝트에만 외곽선이 그려지도록 만들 수 있다.

 

그리고 아웃라인 셰이더보다 스카이박스가 나중에 그려지기 때문에 셰이더가 잘리게 된다.

이때는 아까 추가한 Render Objects의 Event를 AfterRenderingSkybox로 설정해주면 잘리지 않게 만들 수 있다.

기본 세팅

추가로, Cull Front를 추가해줘서 뒷면을 그리도록 해줘야한다.

Cull Front를 잠깐 주석처리 해놓고 보면, 디폴트 값으로 앞면이 그려지게 된다.

Cull Front를 다시 원상태로 돌려놓으면 이렇게 뒷면만 그려진다.

 

 

위와 같은 이미지의 아웃라인 셰이더를 3가지 방식으로 만들어보자.

 

1. 월드 노말 방식, 카메라 거리에 따른 원근 교정 X

float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
float3 normalWS = mul(UNITY_MATRIX_M, input.positionOS.xyz);
positionWS += normalWS * _OutlineDistance;
OUT.positionHCS = TransformWorldToHClip(positionWS);

제일 간단한 아웃라인 방식이다.

 

버텍스 셰이더에서 Input이라는, 로컬 오브젝트 스페이스의 포지션을 받아오고,

월드 좌표로 바꾼 다음에, (UNITY_MATRIX_M은 unity_ObjectToWorld와 같은 것이다)

거기에 OutlineDistance 만큼의 벡터를 더해주는 방식이다. 즉, 그만큼 이동하게 된다.

OutlineDistance는 property에 있기 때문에 인스펙터 창에서 조절할 수 있다.

 

월드 스페이스에서 특정 거리만큼 버텍스를 밀어준 것이기 때문에 가까이 가면 외곽선이 두꺼워진다.

가까이 갔을 때 얇은 라인이 유지가 될려면 아래와 같은 방식으로 진행하면 된다.

 

2. 월드 노말 방식, 카메라 거리에 따른 원근 교정 O

float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
float3 normalWS = mul(UNITY_MATRIX_M, input.positionOS.xyz);
float distToCam = length(_WorldSpaceCameraPos - positionWS);
positionWS += normalWS * _OutlineDistance * distToCam;
OUT.positionHCS = TransformWorldToHClip(positionWS);

단순하게 카메라까지의 거리를 계산해서 빼주면 된다.

 

 

 

3. 스크린 노말 방식, 카메라 거리에 따른 원근 교정 O

OUT.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
float3 clipNormal = TransformObjectToHClip(input.normalOS);
clipNormal = normalize(float3(clipNormal.xy, 0));
OUT.positionHCS.xyz += normalize(clipNormal) * _OutlineDistance * OUT.positionHCS.w;

스크린 좌표계 기반으로 해서 계산하는 방식이다.

 

여기서 이대로 코드 작성을 마무리하게 되면, 위 사진에서 보이는 것처럼 오브젝트가 가장자리로 갔을 경우 외곽선이 정확하게 나오지 않는 현상이 생긴다.

카메라 공간(스크린 공간)은 화면 밖으로 갈수록 원근이 적용돼서, perspective 왜곡이 생기는 특성이 있다.

그 말은 즉, 벡터가 왜곡이 발생한다는 것이다.

화면 밖으로 가면 노말 벡터가 넘어가 버리는 것을 위에서 확인해볼 수 있다.

 

이건 노말 벡터 자체를 길게 만들어줘서, 카메라 공간의 왜곡에 영향을 받지 않을 정도로 만들어주면 위와 같은 현상이 생기지 않는다.

적당히 큰 값을 곱하면 되는데, 여기서는 100정도를 곱해보겠다.

코드의 두번째 줄을

float3 clipNormal = TransformObjectToHClip(input.normalOS * 100);

와 같이 수정해준다.

 

정상적으로 출력되는 것을 확인할 수 있다.

 

 

 

 

 

 

■BRDF

BRDF의 대표적인 예시

BRDF란 양방향반사도분포함수(Bidirectional Reflectance Distribution Function) 라는 뜻이다.

간단하게 빛이 들어와서 반사되는 것에 대한 반사공식은 모두 BRDF 라고 하면 된다.

BRDF에는 Lambert, Phong, Blinn-Phong등이 있다.

입사각 함수와 반사각 함수를 이용해서 표현되며, 어떤 식으로 계산을 할지에 대해 함수를 만들어 놓은 것이다.

비슷한 것으로 BTDF가 있는데 투과 함수이다.

간단하게 BRDF중 몇 가지를 정리해보자.

 

- Lambert

지난 과제에서도 등장했던 Lambert는 다음 사진과 같이, 받게되는 빛의 양을 코사인으로 계산하는 것이다.

 

 

- Half Lambert

Valve사의 '하프라이프'에서 도입한 라이팅 방식으로, 기존의 Lambert에 약간의 수정을 거쳐서 만들어진 것이다.

물체의 어두운 면을 최소 0.5로 지정하고, 기본 라이팅 0~1 사이 값을 0.5~1사이 값으로 변경한다.

기존의 Lambert 함수 결과에 0.5를 곱하고, 다시 0.5를 더해 0~1의 값으로 변형시키는 방식이다.

오브젝트의 어두운 부분이 너무 어둡게 보이지 않도록 해주는 장점이 있다.

때문에 GI(글로벌 일루미네이션) 처럼 보이기도 한다.

 

- Phong

노말 벡터와 입사 벡터를 넣으면 반사 벡터가 나오는 함수인 reflect 함수가 있다.

reflect 함수로 반사 벡터를 구한 뒤, 반사 벡터와 뷰 벡터(카메라 기준)의 각도 차이를 구한 다음,

그 각도 차이에 의해 얼마나 하이라이트가 맺히게 되는가를 계산하는 것이 바로 Phong 셰이딩이다.

 

- Blinn Phong

위의 Phong 셰이딩에서 발전된 개념인데, Phong보다 훨씬 가벼워서 더 많이 쓰인다. (오히려 Phong은 거의 쓰이지 않는다고 한다.)

뷰 벡터와 조명 벡터의 중간값(즉, 그 둘의 합)인 하프 벡터를 구한 뒤, 이를 노멀 벡터와 dot연산 해주는 방식이다.

 

블린 퐁 방식은 퐁에 비해 가장자리에서 원형을 잘 유지한다.

 

■프레넬 (Fresnel), 혹은 림 라이트

가볍고 단순한 방식으로 프레넬을 구현해보자.

 

기존의 램버트 셰이딩에서는 계산할 때 광원을 기준으로 계산하는데, 만약 그것이 카메라 기준으로 바뀐다면 자연스럽게 프레넬을 구현할 수 있게 된다.

 

dot 결과를 saturate해서 0~1로 만든 뒤, OneMinus(반전) 처리 해주면 프레넬이 완성된다.