본문 바로가기
Shader

10주차, 툰 쉐이더를 만들어보자.

by JDCM 2023. 6. 19.

까짓거 만들어보죠!

 

 1. 툰 쉐이더 이전에...

( https://www.virginiamercury.com/blog-va/mountain-valley-pipelines-cost-rises-to-5-5-billion-completion-pushed-to-2020/)

 

우린 먼저 엔진 내의 파이프라인(Pipeline)에 대해 이해할 필요가 있다.

 

컴퓨터에서는 정보를 가져올 때 단계를 거치고, 그 단계를 따라갈 때 마치 파이프 라인처럼 병렬적으로 이행하게 된다. 말 그대로 차례차례 단계를 거치는 것이다.

 

이와 같이 파이프라인은 시스템 내에서 정의된 명령문을 병렬화 된 상태로 단계를 거쳐 처리하는 하드웨어 처리 방법 이라고 생각하면 된다.

 

데이터가 불러와져서 모니터에 출력되기까지 거치는 단계라고도 이해해도 좋다.

 

파이프라인 안에서도 다양한 방식과 단계가 있지만, 우리는 DX9를 중점으로 살펴볼 필요가 있다.

 

근데 이걸 왜 이해해야만 할까?

 

1-1. 그래픽 파이프라인 

(https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipeline_basics/Introduction)

 

 

데이터를 불러오는 과정에서 쉐이더가 어떠한 부분에서 출력되게 되는지를 이해할 수 있다는 것이다.

 

물론 출력 과정에 있어서 다양한 숫자의 배열과 프로그래밍적 영역이 추가되는 것은 사실이지만 그 사이에서도 그래픽이 이용할 수 있도록 빼어둔 부분이 있다는 점이다.

 

우선 그래픽 파이프라인을 이해할 수 있게 설명하자면…

 

 

  1. 버텍스의 정보를 받고, 삼각형 형태로 잇는다.
    컴퓨터 내에서 그래픽 요소를 가져올 때에, 버텍스의 정보들이 숫자 형태로 불러와지면 컴퓨터는 각 버텍스마다의 정보를 “선”으로 연결한다.
    선으로 연결할 때에는 가상의 공간 내에서 버텍스들을 삼각형 형태로 잇는다.
    ( 마치 상단에 예시로 가져온 숫자 연결놀이와도 같다. )

  2. 정점 쉐이더(Vertex shaders) 처리를 한다.
    정점(꼭지점), X ,Y, Z의 좌표 또는 색상, 텍스처, 조명 정보 등 버텍스가 가진 데이터 정보값을 변환시켜서 해당 값들을 변경하게 된다.
    변경할 수 있는 값으로는 위치 옮기기(좌표), 텍스처 바꾸기(투영), 색상 바꾸기(조명) 등이다.
    다만 기존의 정점이나 새로운 정점을 추가하는 등의 작업은 불가능하다.
    ( 위키백과 : 버텍스 셰이더 )

  3. 조명 연산과 컬링(보이지 않는 면) 처리를 한다.
    이를 래스터화(Rasterization)이라 한다. 우리가 포토샵에서 자주 보았던, 래스터화와 동일한 단계인데 이는 “픽셀화”의 단계이다.
    벡터 상태로 머물고 있는 상단의 단계들의 형태들을 모니터에 출력할 수 있도록 픽셀화 하는 것이라고 이해하면 좋다.

  4. 픽셀 쉐이더(Pixel shaders) 처리를 한다.
    렌더링이 필요한 각각의 픽셀들의 색을 계산하는 단계다.
    각 한 픽셀마다 색, 빛, 범프(Normal) 매핑, 그림자, 반사광, 투명 처리등 텍스처의 처리를 주로 맡는다.
    픽셀 셰이더 자체로는 픽셀 하나를 처리하는 영역이기 때문에 주변의 픽셀 또는 도형의 전체적인 부분은 이해하지 못하므로 복잡한 단계는 어렵다.

    픽셀 쉐이더의 경우 어셈블리어, Cg, CLSL, HLSL 프로그램을 사용하는데… 맞다! 우리가 지금까지 사용했던 언어들이고 우린 지금까지 픽셀 쉐이더에서 놀았던 것이다.

    참고로, 텍스처 말고도 깊이(Z버퍼, Z Buffer)의 영역에도 사용되는데… 이는 나중에 알아보도록 하자.

 

지금까지 픽셀 쉐이더에서 재밌게 놀았다고 한다면,
툰 쉐이더를 구현하기 위해서는 버텍스 쉐이더의 단계를 건드릴 필요가 있기 때문이다.

 

( 만약 조금이라도 이해했다고 한다면 스스로를 도닥여주자. 잘 했다.)

 

문득 의문이 드는 것은 파이프라인이라 한다면 프로그래머의 영역이라 생각하고 있었을텐데, 이는 프로그래머들이 그래픽이 다룰 수 있도록 부분 허용을 한 것이기 때문이다.

다만, 여전히 그래픽이 이해하기위한 범위로써는 조금 힘이 들지만 알아두면 나쁠 것이 없다는 것이다. 쉐이더 영역을 빼곤 프로그램들이 다 해줄테니 겁먹지 말자.

 

자, 그럼 이제 천천히 툰 쉐이더의 기본!

외곽선에 도전해보자!

 

2. 외곽선 만들기!

그치만… 림 라이트로 된다면서요!!

 

 

( 어딜 보십니까? 그건 잔상입니다만 ... .)

 

물론! 림 라이트로도 가능하다. 하지만 우리가 원하는 뚜렷하고, 예쁜 외곽선을 나타내기에는 조금 무리가 있다.

(물론 가능하지만, 아무튼 지금은 안돼.) 

우선 평소에 해왔던 그대로 자연스럽게 쉐이더 코드를 정리해주고 시작해보자.

 

 

그리고 #pragma 설정에 vertex:vert를 추가해주자.

 

vertex:vert는 상단에서 이야기 했던 정점 쉐이더(Vertex shaders)의 영역을 건드리겠다는 소리다.

우리가 surf, 즉 surface 영역의 픽셀 쉐이더(Pixel shaders)를 다뤘다고 한다면…

정점 쉐이더를 건드리겠다고 선언함으로써 void surf와 같이 동일하게 void vert 영역을 만들어줘야 한다.

 

 

다만 void surf에서 다룰 수 있었던 코드들이 있듯이, 상단의 appdata_full을 불러오면서 정해진 코드들이 있다. 외우도록 하자!

 

appdata_full 은 정점 데이터의 구조체로써
위치, 접선, 법선, 4개의 텍스처 좌표와 색상으로 구성된다.

  1. float4 vertex
  2. float3 normal
  3. float4 texcoor : UV용 코드
  4. float4 texcoord1,2,3
  5. float4 tangent : 탄젠트 벡터(노멀 맵핑)
  6. float4 color 

 

실제로 버텍스 코드를 짜지 않아도 유니티가 임의적으로 계산해주었지만, 접근한 것들은 우리가 따로 설정할 수 있는 부분이다.

 

그럼 vert 내에서 vertex의 값을 한번 이동 시켜보도록 하자.

vertex가 float4로 선언되어 있으면 x, y, z, w값이 존재한다는 것을 알 수 있다.

그럼 y값으로 1을 더해주면 어떻게 될까? 

 

 

놀랍게도 시도했더니 이렇게 되었다.

정상적으로 Unity에서 불러온 구의 경우 y값으로 정상적으로 위로 올라갔지만 우리 Kyle의 경우 앞으로 툭 튀어나간 것이다. 이건 무슨 문제냐면…

 

Kyle의 모델링의 Local 축의 문제이다. 모델링 과정에서의 Y 좌표가 뒤집어져 있기 때문에 Kyle 모델링 기준의 Local 기준으로 나아가게 된 것이다.

Kyle은 잘못이 없고, 축의 문제이므로 이는 모델링을 수정하여 해결할 수 있다.

 

그 이전에, +1 을 해주었는데 상당히 많이 앞으로 또는 위로 나아간 기분이다.

+1 의 기준은 즉 단위 벡터와 같이 유니티의 1의 기준, 즉 1m로 나아가게 된 것이다.

 

뿐만 아니라 요상한 점이 하나 더 있다.

잔상(그림자)가 남아있다. 바로 standard의 atten의 값이 남은 것인데, 이는 vertex의 처리가 아닌 pixel의 처리이기 때문에 따라가지 못한 것이다! 

 

다행이도 이 문제는 쉽게 해결할 수 있다.

#pragma에 추가했던 vertex:vert 뒤로 addshadow만 넣어주면 된다.

 

( 좋아! 지금까진 잘 따라오고 있어! )

이제 버텍스 쉐이더의 기본기를 이해했다면

우리가 만들 2-Pass 라인 렌더링에 대해서 알아볼 필요가 있다.

 

( https://www.codinblack.com/outline-effect-using-shader-graph-in-unity3d/ )

 

2Pass 라인 랜더링은 Pass, 즉 두 번 보여준다는 형식이다.

한 번 불러온 모델링보다 노말 방향으로 더 크게 한(버텍스가 이동이 된) 모델링을 겹쳐내어 아웃 라인이 보이는 것처럼 처리하는 것이다.

 

문제는, 같은 모델링을 노말로 확장시켜 사용하는 것에는 문제가 있다.


2Pass 기술에서 아웃 라인이 되는 모델링의 경우 노말을 뒤집어 사용한다. 

 

노말을 뒤집을 경우 문득 우리가 MAX나 Z-Brush에서 마주쳤던 문제가 하나 있는데 그게 바로 “보이지 않는 면도 보이게 하기”의 문제였다.

 

즉 Two Face 처리(양면 보기)의 문제인데 파이프라인은 보이지 않는 면을 처리하지 않는 것에 대해서 Backface culling 이라고 명칭한다.



Backgace culling을 하게 되면 보이지 않는 면은 화면에 나오지 않게 되는데,

보이지 않는 면, 보이는 면 둘 다 보고 싶다면 culling을 사용하지 않으면 되는 것이다!

 

그리고 각각, 출력하고 싶은 상황에 따라 처리하는 구문이 있는데...



Cull Back : 시점에서 반대 면의 폴리곤을 보여주지 않는다. (기본)

Cull Front : 시점에 존재하는 면의 폴리곤을 랜더링 하지 않는다 (반전)

Cull Off : 둘 다 그린다.
(놀랍게도 ‘특수한 효과에 사용한다’라 쓰여져 있는데 이거… 이펙트 아닌가?)
( https://docs.unity3d.com/kr/530/Manual/SL-CullAndDepth.html )

 

 

아무튼!!! 외곽선을 만들려면 외곽선이 될 녀석은 뒤집혀야(Front) 한다는 것이다!!!

 

 

Cull front로 뒤집어 주었는데… 문제는 빛은 여전히 잘 받고 있다.

이것은 면은 뒤집었지만 Normal은 뒤집지 않은 문제로 노말도 같이 더해주면 된다.



 

( 아니 SeeU 아버님, 당신이 거기서 왜... )

 

괜찮다. 놀라지 말자.

Normal의 단위 벡터(1m)가 버텍스에 더해져서 각 버텍스가 1m씩 성장(?)한 모습이다.

이 부분은… 우리가 알아서 값을 넣어서 조절할 수 있도록 Properties 값을 넣어주자.

 

 

그럼 이제 버텍스의 확장 값을 마음대로 변경할 수 있다! (장난감을 소중히)

근데 하나 문제가 있다. 

이렇게 Vertex를 조절하는 코드를 완성하긴 했는데 CGPROGRAM ~ ENDCG까지의 코드가 전부 끝나버렸다. 이 상황에서 surf 단위를 조정해도 vert의 영향을 받아 괴상하게만 나오게 된다...

 

2Pass는 두번 그린다고 했는데 어떻게 두 번 그려야 하는걸까?

방법은 의외로 간단하다.


(콩을 가져오면…)

(콩을 가져오면…)

말 그대로 두 번 쓰면 된다.

CGPROGRAM~ENDCG의 코드들을 

한 번 더 쓰면 된다는 것이다.

그럼 한 코드에 두 개가 돌아가는 것이다!

 

그럼 최종 코드와 함께 작업물을 봐보자!

 

 

Properties는 공유하기 때문에, 같이 사용해야 하고 Properties 내의 값들은 사용할 CGPROGRAM 안에 선언해줘야 한다.

참고로 우측 아웃라인의 값은 무려 0.005이다. 

 

근데 여기서 문제가 하나 더 있다.

 

3.  단순한 음영 주기!

그치만… 지금 음영은 너무 리얼한걸요!

 

( 으...으아악! 이게 아니야! )

 

우리가 소위 ‘툰’이라고 부르는 것은 다수 음영이 단순하고 뚜렷하게 되어있기 때문에 현재의 음영 단계에서는 툰쉐이더라고 부르기 힘들다.

너무 리얼했다간 2D 오타쿠도 3D 오타쿠도 떠나기 쉬우니 지금까지 만들어두었던 NdotL로 쉽게 구현해보자.

3-1. IF문 알기

 

명암 셀 셰이딩을 알기 전에, 우선 IF문을 알아둘 필요가 있다.

IF문이라는 것은 “만약에” 특정 조건에 들어 맞았을 때,

그 조건이 맞으면 원하는 값으로 출력해주는 구문이다.

 

else if의 경우 if의 나머지, 또 else if에 충족되지 않는 것은 else로 처리하게 된다.

 

그럼, if를 NdotL에 넣어 사용해보자. 

 

 

만약 노말 벡터와 빛 벡터의 닷 연산이 특정 수치일 경우 NdotL의 결과를 단순하게 나타낼 수 있게 나타낸 것이다.

 

명암을 단순화 했다면 finalColor를 통해 전부 합쳐보도록 하자! 

 

 

합쳐 보았는데 어찌 이상한 느낌이 난다. 깜덩이가 이상하게 묻어나는 경우가 있는데 이 부분은 바로 atten의 흔적이다.

atten의 경우 NdotL으로 처리되지 않았기 때문에 일반적 명암으로 나타나게 되었다. 

이런 경우 툰 쉐이딩에서는 atten을 제외하는 일이 많다.

 

또 문제는 …

 

if문은 쉐이더 내부에서 잘 쓰지 않는다.

if의 경우 명시한 조건이 맞는지를 확인하기 위해서 여러번 계산을 반복하게 된다.

그러다보니 무거워지는 것은 순식간이다.

 

우리가 쓴 if의 경우 굉장히 단순하기 때문에 괜찮다 생각할 수 있지만 if문을 돌리고 나서 이 문서를 쓰는 와중에 내 컴퓨터는 힘들다며 키보드 타자를 씹을 정도다. 

 

3-2. Ceil문 알기

(  https://app.roll20.net/forum/post/2174117/additional-math-functions-for-character-sheets-and-macros  )

 

 

Ceil, 즉 올림(Ceiling, 최대)함수는 무조건 정수로 만들어주는 함수이다.

0의 경우 0이겠지만, 0.1만 되어도 1로 올려버리는 무지막지한 함수이기 때문에 소수점으로 남은 명암 단계가 전부 정수가 되어 나뉘어지는 것이다.

 

if문과 다르게 Ceil의 경우 이미 내재되어 있으므로 한 줄이면 된다.

 

 

Power 값을 따로 Properties에서 수정할 수 있게 해두었다.

 

4. 이제 다 합치기!

(클릭해서 보세요!)

 

 

사실...그림자에 지는 하프톤 쉐이딩을 도전하려 했지만,

그러려면 StandardOutput이 아닌 다른 Output을 설정해야 하고

아직 WorldPos나 ScreenPos를 Output에 불러오는 방식에 대해 이해하지 못한 모양이었기 때문에 적용해보지 못했다.

(나중에는 꼭 시도해보고 싶다.)

 

생각보다 Normal이 거친 것보다 부드러운 것이 더 예뻤고 (Bump값 0.2!)

스펙큘러는 작으면 작을 수록 이뻤다. (무려 power 값이 3000까지 올라갔다…(무식))

하드 서페이스에 따라 라인이 끊기는 모습이 불편해서 모델링을 할 때 참고해야겠다 싶었다.

생각보다 명암이 단순(Ceil 2~4 값)한 것이 “툰”처럼 느껴져서 신기했다.

다만 툰 각각의 명암이 조금 더 부드러우면 어떤 방식이 좋을까 여럿 고민하게 되었다.

(고민하게 되었다는 것은 성공하지 못한 흔적이다…)