炸殺
카테고리
작성일
2020. 6. 16. 08:28
작성자
炸殺
랜더링 파이프라인 (Rendering Pipeline)이란?

 

 

파...이프?

랜더링 파이프라인(Rendering Pipeline)은 3차원 이미지를 2차원 레스터 이미지로 표현하기 위한 단계적인 방법으로,

간단하게 설명하면 우리가 열심히 입력한 데이터들을 모니터에 출력하기 까지의 과정입니다. 

 

 

 

 

 

DirectX 9.0 Pipeline

 

가장 먼저 버텍스 정보(Position, Normal, Color 등등..)를 불러온 뒤 [=정점 데이터],

해당 정보를 바탕으로 버텍스를 이어 삼각형의 폴리 곤으로 만들어 줍니다. [=삼각형 분할]

(모델링할 때 엣지가 삼각형이 되도록 해야하는...이유,,?)

 

그 다음으로 화면에 메쉬가 어떻게 보여질지 결정되는 것이 정점 쉐이더(Vertex Shader) 부분에서 이루어집니다.

로컬 좌표가 월드 좌표에 적용이 되고.... 그게 또 카메라에 적용되고 등등등... 

 

이 정보를 바탕으로 화면에서 짤렸는지 안짤렸는지(클리핑) 뒷면인지 아닌지 판단합니다.(후면선별)

 

레스터화는 포토샵을 익히 사용했다면 익숙한 이름입니다.

주로 벡터이미지를 픽셀이미지로 변환하는 것을 레스터화라고 부르는데

랜더링 파이프라인에서 레스터화는 앞선 정보들을 모니터 픽셀 하나하나에 찍어내어 랜더링(출력)하는 것을 의미합니다.

(레스터라지져라고도 부릅니다. 어찌보면 같은 의미...)

 

드디어 출력했습니다! 픽셀이 되었어요! 이 단계를 마치면 여기서 부터 2D입니다!

 

 

그리고 지금까지 이전 포스트에서 지지고 볶고 열심히 공부했었던 것은 전부 픽셀 쉐이더(Pixel Shader/Surface Shader) 단계로,

텍스처를 넣고 셰이딩을 연산하는 곳이죠.

 

바로 이 부분!!

 

이 후 마지막으로 진행되는 것이 알파테스트/블랜딩, 깊이, 포그 등등등... 의 친구들 (다 그러놓고 마지막에 안그릴거 정함)

 

쉐이더 코드를 통해 우리가 조작할 수 있는 단계는 정점 쉐이더와 픽셀 쉐이더 입니다.

정점 쉐이더 부분에 라이트를 넣으면 버텍스 라이트, 픽셀 쉐이더에 라이트를 넣으면 픽셀 라이트!

 

요즘은 더 발전한 DiretX 11을 사용하기 시작했다. 하지만 쪼랩인 우리에겐 아직... 어렵기에...

 

 

유니티에서는 자체적으로 커스텀 쉐이더를 제작하였을 때

라이팅 옵션에 따라 정점 쉐이더 영역에 넣을지 픽셀 쉐이더 영역에 넣을지 자동으로 결정해줍니다.

(물론 원하는 곳에 직접 만드는 것도 가능!)

 

이 두 곳 중 NPR의 대표 주자, 아직까지도 많이 쓰이는 2Pass 방식으로 외각선을 구현할 때 건들일 곳은 바로 정점 쉐이더 단계. 

 

 

 

 

 

 

 

 

Vertex Shader 

 

DX9스타일로 2pass로 외각선을 만들기 위해 가장 먼저 Vertex Shader를 가동시켜줍니다.

 

 

 

#pragmavertex:vert를 작성하여 버텍스 쉐이더가 작동되도록 선언해주고,

 

 

픽셀 쉐이더와 마찬가지로 void함수를 이용하여 선언해줍니다. 이 때 inout과 appdata라는 두 개의 파라미터를 가져오게 되는데,

이중 appdata는 언더바(_) 뒤에 붙는 선언에 따라 받아오는 데이터가 달라집니다.

 

https://docs.unity3d.com/Manual/SL-VertexProgramInputs.html

만약 버텍스 컬러가 필요없다면 _full을 불러올 필요는 없겠죠. 어떤 데이터를 사용할 건지에따라 적절히 선택해줍시다!

지금은 외각선의 컬러를 바꿀 수 있도록 _full로 불러왔습니다.

 

 

 

 

 

 

2pass의 자세한 설명은 이쪽! https://celestialbody.tistory.com/13

지난 포스트에서 설명했듯이 2pass는 면을 뒤집은 뒤 노말 방향으로 버텍스 포지션을 이동하고,

원본 오브젝트를 한 번 더 그려 외각선을 생성하는 기법이었습니다.

 

그럼 버텍스는.. 어떻게 이동시킬 수 있지..? 그것도 노말 방향으로...

분명 appdata_full엔 버텍스 정보가 들어있었죠, 먼저 버텍스를 움직여보는 것부터 해봅시다.

 

 

 

이동했다..!

버텍스를 받아와 y값에 1을 더했더니 오브젝트의 포지션은 그대로인데 오브젝트가 위로 이동했습니다.

이는 버텍스의 포지션이 이동한 것으로 y값에 더해줬기 때문에 위로 이동한 것입니다.

(유니티의 단위는 미터이기 때문에 1을 더해주면 1m가 이동합니다) 

 

여기에 sin 함수와 time을 넣어주면....

 

잘 움직이는데.. 이상한 그림자가 있다.

저렇게 원래 자리의 그림자가 묻어나오는 이유는 그림자에는 따로 연산을 해주지 않았기 때문입니다.

여러 연산을 마지막에 합친다...라는 것이 실감이 됩니다.

 

나 그림자 연산 안했는데????

알게모르게 쉐이더에서는 빈 공간에서 수 많은 연산을 처리하고 있습니다.

 

#pragma 뒤에 addshadow를 넣어주면 버텍스에 들어간 연산을 카피하여 그림자 연산에도 적용해 줍니다.

 

이렇게 버텍스 포지션을 움직여버리면 Static오브젝트도 움직이게 만드는 것이 가능합니다.

 

 

 

 

 

 

응용 - 버텍스 컬러를 이용하여 원하는 부분만 움직이게 만들기

 

 

이를 응용해서 나무가 바람에 흔들리는 효과를 넣고 싶은데....

나뭇잎만 흔들려야지 나무가 통째로 흔들리면 곤란합니다.

또 얇은 잔가지는 많이 흔들리고 나뭇가지가 두꺼울 수록 덜 흔들렸으면 좋겠고...

마지막으로 동시에 다 똑같이 흔들리면 분명 어색할 거에요.

 

 

 

0.3은 속도 조절을 위하여 곱해주었습니다.

 

기억하시나요...? 색은 방향.....방향은 색.... 

움직이길 원하는 부분에 버텍스 칼라를 칠해주고, 해당 버텍스 컬러의 체널을 sin(_Time)에 곱해주면...

쫌 징그럽지만...

조금만 움직이고 싶은 부분은 색상값을 옅게 넣어주면 됩니다.

레드는 좌우, 그린은 상하, 블루는 앞뒤로 움직이는 것을 확인할 수 있습니다.

 

 

하지만 다 똑같은 시간동안 똑같은 간격으로 움직인다면 너무 이질적일 겁니다.

그렇다고 일일히 속도값을 다르게 지정해주기엔 메터리얼의 개수가! 시간이! 

 

여기서 다시 상기할 점은 색도 결국 0과 1사이의 값이라는 겁니다.

 

같은 오브젝트에 같은 머테리얼, 같은 쉐이더.

한 쪽은 레드만, 한쪽은 레드 값에 그린 값을 더한 노란색을 부분적으로 칠해주었습니다.

 

 

노란색을 칠한 부분은 R값에다가 G값이 추가가 된 상태 입니다.

여기서 _Time에 v.vertex.g를 곱해주면 G값이 있는 곳은 0~1값만큼 R값만 있는 곳보다 빨리 이동하게 됩니다.

 

 

같은 오브젝트, 같은 메터리얼, 같은 쉐이더임에도 버텍스 컬러를 다르게 주니 서로 다른 움직임을 보여주게 되었습니다.

빨갛게만 칠했을 때보다 좀 더 물 흐르듯 자연스러워 보이기도 하네요.

 

나무 뿐만아니라 깃발이나 풀 등...

안움직이면 어색한데 본박에서 애니메이팅 하기엔 너무 부담스러운 친구들에게 사용하면 딱일 것 같습니다.

 

완전 혜자 버텍스 컬러 안쓰면 손해다! 

 

 

+여담으로 Lerp함수를 이용해서 똑같이 만들 수 있지만 걍 곱하는게...더 간단하다.

 

(++버텍스 포지션 UV위치로 바꾸는 것도 추가하기)

 

 

실컷 버텍스 쉐이더를 가지고 놀았으니 이제 진짜 2pass로 외각선을 만들어봅시다.

 

 

 

 

 

 

 

2pass

 

2pass는 말 그대로 '두 번' 랜더링을 하여 외곽선을 만드는 기법이었습니다.

같은 오브젝트를 두 번 랜더링 하기 위해서는 CGPROGAM부터 ENDCG까지 복사하여 아래 붙여넣으면 됩니다.

진짜로

2pass는 Forward Render에서만 가능한 기법입니다. 언리얼에선 외각선을 Pos-tprocess로 처리하는 이유.

 

 

 

 

 

1pass

 

첫번째 단계는 외각선이 되어줄 친구의 면을 뒤집는 것입니다.

3DMAX에서는 backface culling을 체크하여 까맣게 처리되던 뒷면을 아애 투명으로 만들어 표시하지 않을 수 있습니다.

쉐이더 코드에서는 cull back 이라고 부르며 아무런 설정을 하지 않았을 때 기본적으로 들어가는 상태입니다.

 

 

뒷면을 안보이게 하는 것이 cull back.... 그럼 앞면을 안보이게 하려면?

 

 

 

CGPROGRAM 상단에 cull front를 입력하면 앞면은 투명해지고 뒷면이 드러나게 됩니다.

앞면이 아닌 뒷면을 보여주는 것 뿐, 노말은 그대로이기 때문에 라이팅은 이전과 똑같이 적용됩니다.

(라이팅 연산은 노말 벡터와 라이트 벡터를 내적한 것이기 때문!)

 

 

 

 

 

외각선으로 사용되는 1pass의 오브젝트는 2pass에서 그려질 원본 오브젝트보다 커야

2pass 오브젝트에 가려지지 않고 표시 될겁니다.

하지만 단순히 Scale을 늘려버리면 어긋나기 때문에 Vertex를 노말 방향으로 이동 시켜주어야합니다.

 

위에서 vertex의 y값에 1을 더했을 때 1미터 만큼 위로 올라갔던 것처럼 Vertex의 좌표에 Normal을 더해주면

 

잠시 cull front는 주석처리 해주었다.

Normal Vector는 일반적으로 Normalize 되어있는 단위 벡터이기 때문에 그 길이가 1입니다. 

Unity의 단위는 미터(meter)이기 때문에 1m만큼이나 확장되어 버린 것이죠.

 

원하는 수치를 곱해주어 선의 두께를 조절해줍시다.

 

 

 

외부에서 선 두께 조절을 할 수 있도록 프로퍼티에 빼주었다. cull front도 다시 하자.

 

 

외각선의 역할인 1pass는 그림자 연산등 사용하지 않을 경우 모두 제거하여 최대한 가볍게 만들어주는 것이 좋습니다.

 

 

 

 

 

 

 

 

2pass

 

지금 상태에서 원본 오브젝트를 활성화 시킨다면 분명 원본 오브젝트도 cull front 된 상태로 출력이 됩니다.

1pass 때 선언 했던 cull front가 2pass에도 적용된 것으로 CGPROGAM 위에 다시 cull back을 선언해줍니다.

 

 

 

이렇게 2pass 기법을 이용한 외각선 만들기는 완료입니다.

여기서 더 나아가 NPR의 특징 중 하나인 '극단적인 명암 단계'도 넣어봅시다.

 

 

 

 

 

 

if문과 ceil

 

NdotL으로 명암을 연산했을 때는 부드럽게 그림자가 지게 됩니다.

이 친구의 명암을 극단적이게 만들기 위해 우선 익숙한 Costum 라이트를 만들어줍니다.

 

부드러운 명암을 위해 하프램버트 공식을 적용하고 좀 더 선명하게 해주기 위해 제곱해줬습니다.

 

 

 

 

 

 

if(조건){결과값} else{그 외의 결과값}

 

 

 

if문을 이용하여 특정 구간에 대한 결과값을 하나의 값으로 통일하면서 명암의 단계를 단순화 시킵니다.

 

 

else의 경우 (이론상) 무한정으로 사용할 수 있지만

if문은 처음부터 끝까지 다 계산을 마친 뒤 맞는 값을 도출하기 때문에 비효율 적이고 무거운 친구입니다.

 

 

 

컬러도 프로퍼티로 빼서 외부에서 조절할 수 있도록 만들어 주었습니다. 

명암의 단계가 확실하게 나눠지면서 만화같은 음영단계가 완성되었습니다!

 

 

 

ceil (소수점 이하 올림 할 수)

 

ceil함수는 소수점 이하를 올림하는 함수로 0.1~0,9의 숫자를 1로 만드는 함수 입니다.

만약 ndotl에 4을 곱한 뒤 소수점이하를 올림하고, 다시 4으로 나눠주면

 

값이 곱해진 뒤 올림이 되어 1~4까지의 정수로 나뉘게 되고, 그 상태로 다시 값을 나눠

4단계로 명암이 깨끗하게 나눠지게 되었습니다. 

 

곱하고 나눠줄 값을 프로퍼티로 빼내어 명암 단계를 조절 해줄 수 있지만,

명암의 간격이 일정하게 밖에 되지 않는다는 단점이 있습니다.