본문 바로가기
Shader

7주차, 빛을 커스텀 해보자.

by JDCM 2023. 2. 9.

구글 문서의 경우, 7주차 이후부터는 비공개 하겠습니다!
티스토리에 업로드 드리는 것이 훨씬 그림도 잘 보이고, 글도 많이 정리되서 업로드 되기 때문입니다 ㅎㅎ

방문해주시는 여러분들 항상 감사드립니다. 쉐이더 관련으로 궁금하신 것은 문의 주세용.

 

 

암튼, 이제야말로 진짜 “쉐이더”를 보여줄때다!

 

1.  커스텀 하기 전에 “벡터”

( 벡터는 결국 힘과 방향이었다. 어? 이건 아닌데?)

 

저번 시간에 알아본대로 벡터의 값은 길이로써의 힘의 크기와 지표로써 방향을 가져 화살표로 표시할 수 있었다.

 

구체적으로 들어가기 전에, ‘단위 벡터(Unit Vector)’에 대해 알아둘 필요가 있다.

 

단위 벡터(Unit Vector)란…
벡터의 크기가 1인 벡터를 뜻한다.
벡터의 방향으로 1을 쓸 때 사용하고, 벡터를 단위 벡터화(벡터 크기 1로)할 때의 행위를 ‘정규화(Normalize)’라고 한다.

 

왜 1로 정의하냐 한다면, 1을 넘어서는 단위의 경우 연산이 복잡해지기 때문이다.

그럼… 벡터끼리 곱하고, 더할 수 있을까?

1-1. 벡터의 더하기!

( 그냥 찐 더하기다.  )

 

쉽게 말하자면 벡터를 하나씩 더하는 것이다.

A벡터가 (0,1)의 값을, B의 벡터가 (1,0)의 값을 가지고 있다고 생각해보자.

그럼 A + B 는 당연히 (1,1)이 된다. 

 

만약 다른 방향을 가지고 있는 벡터가 있다고 하더라도 결국 더해주면 된다.

예시를 하나 더 봐보자.

눈썰미가 좋은 사람이라면, UV의 값을 보았을 때의 색과 같다는 것을 알 수 있다.

우리는 이제 숫자를 색으로 보기로 했다.

(1, 0, 0) 값과 (0, 1, 0) 값을 더하면 Yellow(1, 1, 0)가 나온다는 점을 이해할 수 있다.

 

근데… 만약 한쪽 벡터가 길이가 길다면?






우선 단위 벡터에서 벗어났기 때문에

Y의 값으로 괴팍하게 꺾인 결과를 확인할 수 있다.

 

이를 통해 알 수 있는 것은…

 

단위 벡터를 쓴 결과물은 각 벡터의 사이 각 (즉, 덧셈의 결과의 벡터가 A와 B의 벡터 사이의 각이 동일하다는 것)이 같다는 점이다.

 

만약, 특정 벡터가 더 길게 나갈 경우 좌측의 이미지처럼 각도가 완벽하게 달라지기 때문이다.

 

그렇기 때문에 단위 벡터가 필요하고,

단위 벡터를 만들기 위해 정규화(Normalize)가 필요하다.



그러나 이렇게 더해보았지만, 그렇다고 해서 결과의 벡터 값의 길이가 1은 아니었다.

이 부분을 유의하고 계속 보도록 하자!



1-2. 벡터의 역 방향!

여기 평화롭게 (1,0) 방향으로 나아가는 A라는 벡터가 있다.

만약 이 벡터가 “반대로” 가야 한다고 한다면 어떻게 해야 할까?

바로, -1을 곱해주면 된다!

 

(1, 1)이 있어도 -1를 곱해준다면 (-1, -1)로 벡터가 반대 방향이 된다.

 

< 여기서 잠깐! >
1 빼기(One Minus)랑 헷갈리지 말자!  1 - A 와 -1 x A는 다르다!



1-3. 벡터의 빼기!

결국 빼기도 빼기지만, 막상 A - B로 생각하기에 복잡하다면,

A + (-B) 라고 생각해도 좋다. A는 그저 제 값을 유지하지만, B만이 마이너스가 되고, 최종적으로 상단의 결과물이 나온다. 

 

벡터는 위치에는 관여하지 않기 때문에 결국 A + (-B)를 이미지로 나타내면 대략 이렇다.

 

보자하니 결국 A - B는 B에서 A로 향하게 되는 벡터의 형식이다.

이를 통해 A - B는 Head to Tail 형식으로 알 수 있다.



1-4. 벡터의 스칼라(Scaler) 곱하기!

이름 그대로 스케일(Scale)이다.

즉, 벡터에 일정 숫자를 곱해 그 곱한 만큼 변화시키는 것이다!

두 배를 곱한 벡터(노란색만)의 경우 길이가 늘어났음을 확인할 수 있다.

만약 0.5를 곱한다고 한다면 오히려 절반이 될 수 있다. (나눗셈과 다를 바 없다.)

 

예를 들어 (1, 1, 0) 에 0.5를 곱한다고 생각해보자.

그럼 결과 값은 (0.5, 0.5, 0)이 되어서 조금 더 탁한 노란 색이 나오게 된다.

 

근데 벡터의 곱셈이 이게 끝이 아니다!




벡터 두개를 곱한다고 한다면…

두 개를 같이 곱해야 할텐데 각각 벡터끼리 곱할 수도 없고, 우리는 공식을 모른다.

 

다행이도 이를 위한 공식이 따로 존재한다.

 

바로 각도를 숫자로 나타내는 것이다.

 

각 벡터간의 각도가 몇이냐? 에 따라 곱셈을 처리해주는 것인데, 각도의 차이를 연산해주는 것에 가깝다.

 

서로 얼마나 닮았냐 에 따라서 값이 정해지는데, 아예 겹칠 경우 0, 90도의 경우 1로 처리한다고 한다.

(White = 0, Black = 1)




즉, 각도를 나타내는 공식이라고 이야기를 할 수 있는데 …

해당 공식에서는 이렇게 부른다.

 

1-5. 벡터의 내적과 외적!

우선 중요한 내적을 살펴보자!

 

( 아냐 이 내적 아니야 )

 

상단에서 이야기 했듯이, 간단하게 말하자면 두 벡터의 각도를 나타내는 공식이다.

벡터의 내적(Dot Product)라 불리는데, 형태를 나타낼 때에도 A⋅B로 나타나기 때문이다.

 

문제는 이 Dot 연산, 즉 내적 연산이 어디서 쓰이는가?

 

(&nbsp; https://ece.northeastern.edu/fac-ece/nian/mom/units.html &nbsp;)

 

우리가 흔히 쓰는 Normal에서 쓰인다.

상단의 이미지에서의 방향들이 모두 내적 연산으로 나타난 Normal의 벡터들이다!

  왜 Normal Map에 내적이 쓰이는가?

Normal Map은 결국 면(Face)이다.

Vertex를 통해 면을 구성하고, 그 면들이 바라보는 방향이 결국 벡터나 마찬가지이다.

노말은 그 방향, 즉 벡터의 값을 가진 하나의 면이라는 것이다!

참고로 노말의 경우 평면에서 수직으로 나타나게 된다. 

 

(&nbsp; 6주차 내용을 다시 확인해보자. )

 

우리는 6주차에서 면적에 닿는 빛의 Vector가 같은 수직(직각)일 경우 훨씬 더 밝게 나온다는 점을 알 수 있었다.

결과적으로 Light Vector와, Normal Vector가 같이 마주할 경우 밝게 나온다는 것이다.

 

 Face Normal, Vertex Normal?

 

(&nbsp; https://polycount.com/discussion/132653/hardening-edges-in-max l &nbsp;)

 

면마다 노말을 처리하다보면, 노말 방향이 한 면에 하나이기 때문에 면 전체가 하나의 밝기로 랜더링이 되다 보니 딱딱하게 보인다. (우측의 경우)

 

그렇기 때문에 딱딱한 형태를 버리기 위해 면에 Normal Vector를 붙이는 것보다, 각 버텍스에 가지고 있게 변경하게 된다. 즉, Vertex Color로 처리할 수 있도록 하였다. 

(참고로 사이의 경우는 Lerp(보간)방식으로 수정하도록 하였다.)

 

그러나 각지고 부드러운 재질을 동시에 나타내게 하려면… 둘 다 섞을 필요가 있다는 점을 느낀 개발자들은 결국 Smoothing Groups를 만들게 된다.

 

여기서 잠시 알아갈 필요가 있는 것이 있다.

 

Vertex 자체가 가진 정보는 …
  • Color / UV(texcoord) / Normal / Position … Tangent / Index 등 ...

 

Vertex가 정보를 가질 때, Normal의 경우 각 1개밖에 가지지 못한다.

그런데…한 Vertex 당 많은 Normal을 가져야 한다면?

 

 

그렇다. 그냥 많이 (씀씀이를) 쓰면 되는 것이다.

Smoothing 된 것이 훨씬 가벼울 수밖에 없는 이치다.

 

 그래서 내적은 왜?

Normal Vector가 Vertex를 기준으로 나오게 된다면, 결국 Light Vector와 부딪히면서 “각도의 차이”가 날 것이다. 

만약 각도가 90도로 직각으로 마주하게 된다면 그 부분은 어두워야 할 것이고, Vector 방향이 정면으로 동일하다면 그 부분은 가장 밝을 것이다!

이와 같이 Normal과 Light가 마주하는 부분에서 두 벡터의 각도 값을 알아야 한다.

이래서 내적 연산이 필요한 것이다!



추가적으로 교수님 말씀에 의하면 … Dot Product의 경우 cos 세타...라고 하는데,

 

 

여튼간 우리는 잘 모르겠으니까 Dot 함수가 “코사인 그래프”랑 연관 있구나 를 이해하면 되겠다. (그래픽인 우리가 삼각함수를 알겠습니까?)

대신 알아야 할 점은… 하단의 코사인 그래프의 법칙이다.

 


0º일 때에는 = +1
90º일 때에는 = 0
180º 일 때에는 = -1

 

숫자가 색으로 보이는 우리들은 이제 0이 검정, 1이 하양으로 보이는 눈을 가지고 있으므로 저 이미지의 명도의 모습도 보일 것이다. (착한 사람만 보여요.)

 

아무튼, 이제 좀 만들어 볼 수 있을 것 같으니 코드의 영역으로 고고씽 해보자.

 

2. 커스텀 라이트 만들기

텍스쳐를 빼고 나머지 부분들은 청소를 하자.

 

 

그럼 MainTex만 남은 상태가 된다. 문제는 우리가 궁금한 것은 Custom이다.

 

CGPROGRAM에 위치한 #pragma 에서의 surface surf Standard가 눈에 보인다.

앞에 있는 surface surf 가 뭔지는 몰라도 우리는 우선 Standard를 쫓아낼 것이다.

Standard라는 말이 일단 Custom에서 방해가 되기 때문이다.



 

Standard 의 자리를 GetOut으로 바꿨더니 유니티가 급 

GetOut에는 SurfaceOutputStandard를 불러오지 못하겠으니 샘플러들도 의미 없다고 이야기를 하기 시작한 모습이다.

 

SurfaceOutputStandard 는 Standard 전용이기 때문에 바꿔줘야 하는데 

이 또한 쉽다. Standard만 지워주자.

 

< 구형 라이트용 SurfaceOutput >

struct SurfaceOutput
{
half3 Albedo;
half3 Normal;
half3 Emission;
half Specular
half Gloss;
half Alpha;
}

Standard 기준에서 생략된 가벼운 조명 방식이고, 유니티 4까지는 기본 조명이었다.
나타낼 수 있는 Light의 종류로는 Lambert, BlinnPhong이 있다.

 

아무튼 SurfaceOupt으로 바꿔주면…

Lambert를 쓸지, BlinnPhong을 쓸지 물어본다.

문제는 우리는 Custom Light를 제작하기로 했기 때문에, 우선은 무시하기로 하자.

 

결국 SurfaceOutput도 Lambert나 BlinnPhong을 위한 함수이기 때문에

우리는 Custom 함수도 따로 만들어줘야 한다. 

 

  1.  void surf 아래로 float4를 만들어주고 “Lighting”을 적어준다.
    대소문자가 중요하다. Lighting을 Custom으로 하겠다는 유니티 쉐이더 내의 정해진 규칙이기 때문이다. (Lighting 다음에는 다른 이름들을 적어줘도 좋다.)

  2. float4 LightingGetOut 옆에 SurfaceOutput을 불러온다.
    결국 SurfaceOutput은 존재하고 있기 때문에, 그 공식들을 불러올 수 있다.
    void surf에서는 SurfaceOutput을 o라는 이름으로 가져왔다면
    우리는 s라는 이름으로 가져올 수 있다(가져오는 이름은 마음대로 할 수 있다.)

  3. SurfaceOutput s, 옆으로 float3 LightDir 라는 Light Direction을 가져오고
    그 옆으로 float atten 을 가져온다. (이건 나중에 보자.)

  4. void surf에선 void라는 특정 규격이 있었지만, 우리는 float4이므로 return이라는 돌려줄 값이 필요하다. 

 

 

해당 과정을 작업하면 이런 형식이 된다. 나는 return(돌려줄 값)에 float4(1, 0, 0, 1) 즉 불투명 빨강을 넣어보았다. 그러면 … 



핑크가 된다.

분명 (1, 0, 0, 1) 불투명 빨강이 나와야 하는데 

왜 이럴까? 싶지만

 

주변에 놓여진 Ambient 값을 받고 있기

때문이다. 그러므로 Ambient 값을 계산하지 않도록 

          #pragma surface surf GetOut 뒤에 “noambient”라는 Ambient Light를 계산하지 않는 값을 넣어주자.




그럼 순수한 빨강이 나온다!

 

문제는, return에 들어간 float4의 경우…

우리가 애써 가지고 온 SurfaceOutput 안에 있는

Albedo나 Normal 같은 기본 값들을 사용하지 않고

오로지 return을 통해 float4(1,0,0,1) 빨간색만 무자비하게 뽑은 것이다.

 

return말고, 진짜 SurfaceOutput을 써보자.



2-1. 커스텀 라이트 구체적으로 들어가기

Return을 통해 원하는 값이 출력된다는 점을 알 수 있었다.

그럼 우리는 알베도 값이 출력되기를 원하니까 return 값에는

float4(s.Albedo.rgb, 1); 값을 넣어 불투명 Albedo를 출력시키도록 만들자.

그럼 희멀건...하니 아무것도 없는 하얀 모델링이 나온다.

 

이게 바로 Unlit이다.

Standard 라이팅이 없는, 가장 가벼운 상태의 모습이다.

이펙트를 할 때 가장 많이 쓰이는 라이트 없는 쉐이더 계산이다.

 

이 상태로 Albedo를 불러와서 Texture는 불러올 수 있었다.

다른 것도 한번 봐보자!

 

return float4 (s.Normal, 1);로 불러올 수 있다.

 

< 여기서 잠깐! >
Normal의 경우 방향을 설정해줄 수도 있다.

return float4 (s.Normal.xxx, 1) = R을 바라보는 방향
return float4 (s.Normal.yyy, 1) = G를 바라보는 방향
return float4 (s.Normal.zzz, 1) = B를 바라보는 방향

심지어 World 좌표이다. World에 존재하는 방향으로 언제나 위쪽면이 밝은 식이다.



이렇게 노말까지 보고, LightDir를 통해 Light의 방향도 알게 되었다면

Dot 연산을 할 수 있을 것이다!

  1. float으로 변수를 정의해주자. 굉장히 유명한 변수 이름이 있다.
    “NdotL”(엔다델)이다. 그리고 dot 연산은 프로그램이 해주기 때문에
    dot 뒤로는 노말과 라이트 방향을 넣어주면 된다.
    float NdotL = dot(s.Normal, LightDir)를 선언해주자.
  2. return에서 NdotL 값을 넣고 확인해보면 …

 

 

처음으로 만든 라이트 연산이 돌아가는 것이 보인다!

밝은 부분은 각 Vector가 들어맞아서 (각도값이 작아서) 1로 나타나고,
어두운 부분은 Vector가 틀어져서 (각도값이 커서) 0으로 나타나고 있다.

 

Vector에 대해 상단에서 이야기 할 때 벡터를 뒤집으려면 마이너스(-)를 해주면 되기 때문에, NdotL을 return할 때 앞에 마이너스(-)를 붙여주면, Vector가 뒤집힌다.

비주얼적으로는 음영이 뒤집힌 것으로 보인다. (역방향)

문제는 …

 

우측 방향을 보면 180도로 완벽히 다른 부분에서는 -1까지 값이 떨어진 것이 보인다.

그러다보니 해당 NdotL에다가 +1을 더해도 여전히 까만 부분이 존재할 수밖에 없다.

라이트 자체의 범위가 -1~1이 되어버렸고, Ambient 값을 넣게 되어도 불완전하다!

 

그래서 우리는 0과 1만 나오게 하도록 Saturate 해야한다.

쉽다! NdotL을 saturate 하면 된다. ( NdotL = saturate(NdotL); )


 

그럼 Ambient 값을 넣어도 불안하지 않으면서

동시에 +1을 더해도 0이 남지 않는다.
(이미지는 Ambient 값이 더해진 것이다.)

 

문제는 상단의 이미지를 보았을 때,

명암이 너무 뚜렷하고 딱딱해서

보았을 때 예쁘지가 않았다.

 

여기서 조금 더 발전해보자.

 

2-2. Half Lambert 만들기

하프라이프 신작 언제 나오지?

(참고로 이 글은 알릭스가 발표되기 전 작성한 글이다.) 

 

(&nbsp; https://wallenwang.com/2017/03/lambert-lighting-model/ &nbsp;)

 

각진 부분을 처리해주는 벨브(Valve)의 “마법 공식”이 있다.

 

x 0.5 + 0.5

 

이것은 외워야만 한다!

이 마법 공식을 상단의 이미지에 넣으면 이런 형태로 바뀌게 된다.

 

 

가장 중요한 것은, -1이었던 부분이 0이 되었다!

더해서 0인 상태였던 각지는 음영의 기준점이 0이 아니라 0.5로 변화되었고, 그 주변의 값들도 0.7에서 0.85등 좀 더 부드러운 형식으로 변화된 것을 확인할 수 있다.

 

모델링이 -1의 음영 값 없이 뽀얗고 예쁘게 음영이 졌다.

만약 음영을 더 뚜렷하게 하고 싶다면 제곱(Power) 공식을 쓸 수 있다.

 

NdotL = pow(NdotL *0.5+0.5, 1.5);

 

2-3. Lambert 완성 시키기

Half Lambert를 둘러보았으니, Lambert를 텍스쳐까지 넣어 확인해보자.

 

 

보아하니 Albedo에 NdotL 연산을 넣었다.

Albedo에 넣으면 사실 음영을 받고 있었지만, 우리는 Custom을 통해서 제작했기 때문에 작업한 빛 커스텀을 Albedo에 곱해주어야 한다.

 

왜냐면 빛 연산은 밝아지는 것이 아니라 어두워지게 만드는 것이기 때문이다.

결국 음영(Shader)이다!

 

또 문제는 …

현재는 Directional Light의 Color가 적용되지 않는다.

다행인 것은 색깔 데이터는 아예 코드 변수로 선언이 되어있으므로 추가해줘야 한다.

 

 

그러나 아직 빠진 것들이 두개가 있다.

 

  • 스펙큘러의 경우 엔다델에서는 넣을 수 없다.
    따로 코드를 만들어 넣어주어야 한다.

  • 모델링 자체의 그림자이다.
    이게 바로 atten이다. (Attenuation, 감쇄)
    atten 또한 그림자의 영역이므로 * atten을 해주면 된다.




    정리하자면 ...

 

참고로 둘 다 atten이 들어간 모습이다.

사용한 모델링이 Kyle이기에 atten이 어색하지 않지만,

툰 쉐이딩의 경우 atten이 들어가면 코 하단의 음영이나 입 주변의 음영이 강하게 남아 생략하는 경우가 많다고 한다.

 

마침 졸작 팀의 캐릭터가 툰 쉐이더를 통해 보여지는 캐릭터이므로 커스텀한 라이팅들을 넣어보기로 했다.

 

 

실제로 좌측은 Atten이 들어간 상태이고, 우측은 들어가지 않은

Half Lambert Power 상태가 들어간 모습이다. Atten의 경우 브로콜리의 머리가 너무 앞으로 나온 탓에 그 음영이 강하게 져서 보기에 예쁘지 않았다.

 

 

1번 Lambert, 2번은 Half Lambert, 3번은 Powered Half Lambert이다. (atten 없음)

확실히 부드럽고 그림자(명도)의 양이 적은 Half Lambert(2번)이 가장 보기에 예뻤다.

 

다만 아직 Specular를 포함한 다른 요소들이 없어 그리 예뻐보이지도 않는다.

앞으로의 쉐이더 코딩을 더 기대해보자...

 

'Shader' 카테고리의 다른 글

9주차, 스펙큘러를 만들어보자.  (1) 2023.06.09
8주차, 림라이트를 만들어보자.  (1) 2023.06.02
6주차, 빛을 알아보자.  (0) 2023.01.30
5주차, Vertex Color를 주자.  (0) 2020.10.08
4주차, UV를 다뤄보자.  (0) 2020.10.05