본문 바로가기
Shader

8주차, 림라이트를 만들어보자.

by JDCM 2023. 6. 2.

※ 금번 게시물에는 광과민성(발광/깜빡임/강한 빛)을 유발할 수 있는

이미지(Gif)가 포함되어 있으니 주의 부탁드립니다!

 

Holy 해질 시간이다! 

 

1. 림라이트(Rim Light)?

( 자막은 신경쓰지 맙시다. 드라마 한니발 시즌 2중 한 장면이다.  )

 

피사체 뒤에서 강한 조명을 주어 위, 측면 모서리등을 따라 빛의 테(Rim)이 나타나는 빛을 뜻한다. 소위 우리가 말하는 “후광”에 가깝다. 

 

림라이트를 쓰게 되면 화면과 피사체간 분리를 할 수 있고, 물체의 구조나 재료도 효과적으로 보여주게 된다. 

 

근데 림을 구성하려면 어떻게 해야할까? 우선은 … 

 

1-1. 뷰 벡터 이해, 림라이트 만들기

( https://en.wikibooks.org/wiki/GLSL_Programming/GLUT/Specular_Highlights   )

 

뷰 벡터라는 것은 결국 내가 바라보는 시점(View), 즉 카메라를 기준으로 바라보는 시각의 벡터를 나타낸다.

 

우리는 7주차에 Light Direction, 즉 빛의 벡터를 썼다. Light Direction의 경우 버텍스를 기준으로 라이트가 있는 쪽으로 벡터가(화살표가) 향하고 있었다. 

 

View Direction도 마찬가지로 사물을 바라보고 있기 때문에 (카메라가) 버텍스를 기준으로 카메라가 어디에 있는지 벡터가(화살표가) 향하게 된다.

 

다행이도 그 연산은 엔진이 해주므로, 우리는 공식을 넣을 필요가 없다.

가볍게 viewDir 라는 함수만 쓰면 되겠다.

 

그럼 저번 시간에 배운 Lambert 상태로 쉐이더를 초기화 해보고, viewDir 가 어떤 모습으로 나타나는지 코드를 통해 알아보도록 하자.

해당 코드를 통해 알 수 있는 것은 viewDir가 어떤 식으로 연산되는지는 몰라도

R,G 즉 X,Y 의 값이 보인다는 점은 알 수 있었다. (근데 잘 모르겠다)

여기서 잠깐! 7주차에서 이런 얘기를 했었다.

 

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

 이 내적 연산(Dot Product)는 우리가 노말과 라이트가 마주하는 부분에서 두 벡터의 각도 값을 알기 위해서 사용했었다.

 

대신 우리는 그 때 lightDir를 사용했었는데, 만약 우리가 lightDir 자리에 viewDir를 넣으면 어떻게 될까?!

 

   

( 포스트 프로세싱이 있어서 좀 밝습니다. )

 

o.Emission = dot(o.Normal, IN.viewDir);

lightDir도 결국 벡터 중 하나일 뿐이고, 우리는 lightDir에서 받아오던 벡터 위치를 우리 viewDir로 가져왔을 뿐이다.

 

쉽게 설명하자면 우리가 바라보는 Direction(벡터)가 빛이 비추는(비유하자면) 방향이 된 것이다. 그러므로 우리가 바라보는 방향은 전부 “밝다”

 

참고로, 다시 7주차를 돌아보자면

-1 값이 나오게 되는 부분은 언제나 문제를 일으키므로 0~1로 한정을 만드는 Saturate는 습관화가 되는 것이 좋다.

 

o.Emission = saturate(dot(o.Normal, IN.viewDir));

문제는 우리가 원하는 Rim은 바깥이 어두운 것이 아닌, 바깥이 밝은 형태다.

그렇기 때문에 우리는 이 형태를 뒤집어줘야(One Minus)한다. 

 

o.Emission = 1 - dot(o.Normal, IN.viewDir);

밖이 밝아졌지만, 문제는 어두운 부분과 밝은 부분의 경계가 애매모호하고

림이라고 보기에는 너무 밝은 부분이 많다. 

 

그 이전에 …

Emission에 들어가는 코드 자체가 너무 길어졌으니 정리를 해보자.

 

 Dot연산의 경우 float(float1)으로 계산될 수 있으므로 Emission 값에 들어간 모든 설정을 rim이라는 float1 값으로 바꿀 수 있다.

 

float rim = saturate(dot(o.Normal, IN.viewDir)); // dot 연산을 진행하고
rim = 1 - rim  // rim의 값을 One Minus로 뒤집어 준 다음

o.Emission = rim; // 그 값의 결과를 보여준다.

다시 Rim의 저 애매모호한 검은 부분의 값을 보자면, 0~1사이로 들어가고 있는 저 부분에 power라는 함수를 쓸 수 있다.


Power는 “제곱”이다. 그림을 보자!

 

원형의 중앙점으로부터 밖으로 까지의 0~1 사이의 수가 제곱이 되면서 상단의 숫자는 하단의 형태로 바뀌게 된다.

그럼 자연스럽게 1에 가까워지던 숫자들이 제곱이 되면서 소수점을 가진 수는 더 0에 가까워지게 된다.

 

잘 모르겠다면 Power를 한번 적용해보자.

 

float rim = saturate(dot(o.Normal, IN.viewDir));
rim = 1 - rim;
rim = pow(rim, 3); // power이지만, pow라고 쓴다.

o.Emission = rim;

예상했던 대로 어두운 부분이 더욱 뚜렷해지고, 외곽의 빛이 라인이 되듯 얇아졌다.

현재는 테스트 용을 위해서 3을 주었지만 원하는대로 림을 줄 수 있는 것이다.

 

여기서 추가적으로 응용 할 수 있는 점은

  1. 림 라이트의 Power 값을 Properties로 빼거나
  2. 림 라이트에 들어가는 색깔(Color)값을 Properties로 뺄 수 있다.

 

더하기로 …

Rim Color는 결국 Rim에다가 색을 곱하는 것이다. 만약 Emission의 지금 값에다가 숫자를 곱하면 그만큼 더 Rim에서 0이 아닌 값을 가진 부분이 밝게 빛날 것이다.

 

문제는 Color내에서 0을 곱하게 된다면 결국 Rim은 나오지 않는다.

이런 방식으로 Range 또는 다른 Properties를 통해 Rim을 껐다 켰다 하도록 할 수도 있다.

 

이런 응용 방식은 타격, 또는 특정 상황에 림을 껐다 켰다 할 때에도 사용 가능하다.

 

하단은 추가적 응용 방식을 적용한 점이다.

 

 

와!

그리고 꿀팁을 하나 주자면…

 

    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        [HDR] _RimColor("Rim Color", color) = (1, 1, 1, 1)
...

을 사용한다면, HDR을 통해 Bloom이 이쁘게 먹는 Color를 사용할 수 있다.

 

1-2. 투명한 오브젝트에 림만 나오게 하기

Alpha에 영역에 다시 들어가보자. 

 

(역시 홀로그램은 R2D2...가 아니라 저런 선이 지지직~하는거지)

 

저런 지지직 선을 따지기 전에, 우리는 애초에 투명한 홀로그램의 모습을 위해 Alpha의 영역에 침입해야 한다.

 

문제는… Emission으로 사용했음에도 불구하고 원하는 림만 나오지 않고 애매모호~하게 그라데이션으로 검은 부분이 묻어 나온다. 이 때 할 수 있는 방법은…

 

o.Emission = rim * _RimColor.rgb; // rim을 지워버려!!!

왜냐면, rim에서 나타나고 있는 애매모호한 영역들(0도 아니고 1도 아니여)이 시각화 되어있기 때문에, Alpha는 검은 부분은 전부 제외하므로 Rim을 나타내는 것은 Alpha에게 전부 맡기고, 우리는 Emission에서 Color만 뽑아내면 되는 것이다.


 

HDR 값을 넣게 되면 훨씬 예쁘다.

문제는… 뒤에 있는 보이지 말아야 할 부분도 보인다.

 

이것은 Z 버퍼(Z buffer)의 문제인데, 아직 우리의 지식으로 해결할 수 없다.

(포기하면 편해)

 

그냥 포기하고, 다른 부분을 응용해보도록 하자.

 

1-3. 림 깜빡이게 하기

잠시 사라졌다가 나타나는 것에는 색이 아니라 알파가 0과 1이 되어야 할 것이다!

분명 Emission에 rim을 넣었을 때에도 검은 색이 나왔던 것처럼, 알파 값에 0이 곱해져야지만 현재의 림 라이트가 사라지기 때문이다.

 

( 쉐이더 계의 오비완에게 도움을 요청해보자. )

 

우선 어렵지 않게 알파의 rim 값에 sine 함수가 들어간 Time.y를 곱해보도록 하자.

그럼 림 라이트가 sine값에 맞춰서 꺼졌다 커졌다 하겠지?

엥?

해결책을 찾기 이전에, 애초에 Sine 함수의 그래프를 먼저 볼 필요가 있다.

(  https://www.mathsisfun.com/algebra/trig-sin-cos-tan-graphs.html  )

 

Sine 함수는 -1까지 떨어진다.

그러다보니 Alpha의 값도 결국 마이너스까지 보여지게 되는 것이다.

그럼 Minus로 안 가게 하는 Saturate를 쓰면…? 


깜빡이기를 바랬는데, 

-1이었던 구간이 0이 되면서

0인 구간이 길어지고 말았다.

 

Saturate가 좋은 방법이 아니라면

무엇이 좋을까?...

 

여기서 다시 주목받을 수 있는 것이 있다. 7주차에서 외워야 했던 것이 있다.

벨브(Valve)의 “마법 공식”, Half Lambert 방식이다.

 

x 0.5 + 0.5

0으로 떨어진 부분은 0.5로, -1인 부분은 0으로 만들어줌으로써

이런 그래프가 완성이 된다.

 

 

 

       o.Alpha = rim * (sin(_Time.y) * 0.5 + 0.5);

참고로 _Time.y 뒤로 수를 곱하면 속도를 조절할 수 있다.

Properties로 따로 떼줄 수 있다.

 

근데 … 이게 끝이 아니다! 다른 함수가 있다.

       o.Alpha = rim * abs(sin(_Time.y));

abs란, 절대값(absolute value)로써 음수를 양수로 만들어주는 함수다.

abs의 경우 절대값이다보니 그래프가 0의 경우에선 날카로워져서

Half Lambert와는 다른 느낌이 된다.

 

1-4. 림 라이트에 노말 넣기

홀로그램이라 해서 세부적인 굴곡이 없으면 묘한 법이다.

Normal을 불러오는 것은 지금까지 했던 것과 동일하다.

 

 

그럼 훨씬 디테일해진 우리의 림과 홀로그램에 가까워지는 모습을 볼 수 있다.

문제는… 상단에서 이야기 했듯 홀로그램의 선이 흐르는? 모습이 없다.

 

선을 한번 넣어보도록 하자.

 

1-5. 홀로그램 선 넣기

그 이전에 … worldPos를 이해하자!

 

( 솜솜코 )

 

우리가 viewDir를 가져온 것처럼 worldPos라는 World (Unity의 Scene 내) 포지션을 가져올 수 있다. 상단의 이미지는 부적절하므로 하단의 이미지를 보자.

UV를 보았을 때처럼 유니티 씬 안에 존재하는 Position을 볼 수 있다는 것이다.

(참고로 이걸 볼 때에는 “Opaque” 상태로 “alpha:blend”를 끄도록 하자.)

 

그러나 이렇게 보고 있음 뭐가 뭔지 모르겠다.

한번 다른 부분으로 보도록 하자.



worldPos의 y상태만 본다면, 0점을 기준으로

1(상단)은 하양, -1(하단)은 검정으로 나타나게 된다.

 

오브젝트가 아무리 움직여도 World의 좌표계이므로

뒤집고 돌려봐도 똑같은 형태로 나오게 된다.

 

그런데, 이 상태로 worldPos계에 Time을 주게 된다면?

 

( 하얗게 불태웠어... )

 

사실 하얗게 죽은게 아니다.

계속 y값을 향해 올라가고 있고, 1을 초과했기 때문에 겁나 계속계속 하얘지는 중이다.

 

그러나 이 문제를 위해 좋은 함수가 (또) 있다.

 

o.Emission = frac(IN.worldPos.y + _Time.y);

소수점만 자르는 frac는 “소수점만 남기는” 함수다.

그러다보니 1을 초과해버려도 어차피 다시 0.1로 돌아가기 때문에

결과물은 이렇게 나오게 된다.

( 문~~~ 크리스탈~~~ 파워~~~ )

 

라인의 개수가 부족하다면 IN.worldPos.y 뒤에 숫자 값을 곱하고, 라인이 상단으로 올라가게 하고 싶다면 Time.y 를 빼주면 된다.

 

그럼 정리해보자!

(클릭해서 보세요!)

결과물도 이렇다. (포스트 프로세싱 넣고 설정을 조율했다.)

※ 광과민성 이미지 주의하세요!

 

하단에 응용 코드도 가보자고!

 

가장 문제였던 점은 아직 변수에 대한 이해가 부족해서 worldPos를 쓰게 되면 단 한번밖에 못 쓰는 줄 알았다. 그래서 Line Texture도 따로 썼다(...)

 

그러다보니 Line Texture가 들어간 곳은 UV를 따라 어색하게 흐른다.

 

이 점도 수정해보면서 동시에 worldPos가 아닌

localPos로도 시도해보는 것도 좋을 것 같다!

 

'Shader' 카테고리의 다른 글

10주차, 툰 쉐이더를 만들어보자.  (3) 2023.06.19
9주차, 스펙큘러를 만들어보자.  (1) 2023.06.09
7주차, 빛을 커스텀 해보자.  (1) 2023.02.09
6주차, 빛을 알아보자.  (0) 2023.01.30
5주차, Vertex Color를 주자.  (0) 2020.10.08