炸殺
카테고리
작성일
2020. 5. 24. 22:29
작성자
炸殺

 

Vector란?

 

백터란 무엇일까요? 고등 수학에서 백터와 기하학이란 것을 배웠던 것 같은데.... 저는 기억에 없습니다...

 

 

컴퓨터 그래픽에서 Vector는 [힘][방향]을 표현해주는 도구 입니다.

 

 

방향과 힘을 가지고 있는 벡터

간단하게 설명하자면 화살표의 방향과 그 길이는 힘이라고 보시면 되는데요,

 

 

이러한 백터는 위치에 영향을 받지 않습니다.

즉, 방향과 힘이 같다면 모두 같은 벡터인 것이죠.

 

모두 같은 벡터이다.

 

 

Vector는 Shader에서 float을 이용하여 표기하며

2차원은 float2, 3차원은 float3로 표현 할 수 있습니다.

 

 

2차원 벡터는 float2, 3차원 백터는 float3으로 표시한다.

 

 

벡터는 방향과 힘을 표현하는 친구로 위치의 영향을 받지 않기 때문에 표기할 때 역시 시작점>도착점이 아닌,

시작점을 0으로 고정하여 이동한 방향과 힘만 표시합니다. (0.1)>(0.2) 이렇게 표기 하지 않고 (0.1)이라고만 표기.

 

예외적으로 vector1(float)은 방향 없이 힘만을 가지고 있습니다.

 

 

 

 

 

단위 벡터 (Unit Vector) 

 

 

단위 벡터(Unit Vector)는 크기가 1인 벡터를 부르는 말로,

벡터의 '방향'만 필요하고 크기는 필요 없을 때 사용합니다.

이렇게 백터의 크기를 1로 만드는 것을 정규화(normalize)라고 하는데요.

정규화를 거치지 않는다면 연산이 복잡해지거나 오류 발생, 무거워짐 등 각종 문제의 원인이 되기도 합니다. 

 

 

정규화를 구하는 식은 다음과 같습니다

 

출처 - 교수님 피피티

만 엔진이 알아서 해주니까 외울필요는 없습니다.

 

 

 

 

 

 

 

벡터의 연산

 

 

  •  벡터의 덧셈

 

A(x¹,y¹,z¹) + B(x²,y²,z²) = C (x¹+x²,y¹+y²,z¹+z²)

 

 

여러개의 백터를 더하고 싶다면 어떻게 하면 될까요?

0을 기준점으로 시작하여, 백터 값만큼의 위치로 선을 그어주고, 도착한 위치를 다시 시작점으로 삼아 더하고 싶은 백터의 값만큼 선을 그어주길 반복하면 그 값을 얻을 수 있습니다. 

 

float3(3차원)도 마찬가지!

 

 

 

 

  • 벡터의 뺄셈 

 

 

먼저 벡터에 마이너스(-)가 붙게되면 그 방향이 뒤집어 집니다.

벡터에 -1을 곱해주면 그 방향이 뒤집어진다. 벡터의 곱셈은 아래...

 

A(x¹,y¹,z¹) - B(x²,y²,z²) = C (x¹-x²,y¹-y²,z¹-z²)

 

 

어렵게 보이지만 A-B는 즉 A+(-B)입니다.

백터 A에 방향이 뒤집어진 백터 B를 더해주면 됩니다.

 

 

벡터는 위치에 영향을 받지 않는다고 위에서 언급하였습니다.

 

여기서 주목할 점은 백터A에 백터B를 빼자 B점에서 A점으로 향하는 벡터값이 나왔다는 것입니다. 

 

이를 Head to tail이라고 부르는데요,

A를 플레이어 캐릭터가 있는 위치, B를 몬스터가 있는 위치로 가정했을 때,

플레이어 위치(A)에서 몬스터 위치(B)를 빼면

매 프레임 이를 연산하며 몬스터가 플레이어가 있는 곳으로 쫒아가게 됩니다. (플머 된 기분~~)

 

 

 

 

 

 

 

 

  •  벡터의 곱셉 

 

[ 1. 벡터의 스칼라 곱 ]

 

 

벡터를 곱하는 건 알겠는데... 스칼라는 뭘까?

 

스칼라는 Scaler로 방향 없이 오직 '힘'만 가지고 있는 친구입니다.

방향 없이 힘만 가지고 있는....vector1(float)이네요.

 

이전 포스트에서 설명하였던 float의 특징을 기억하시나요?

float2는 float2랑 연산해야하고, float3은 float3끼리 연산해야만 했지만 float은 예외였습니다. 

float2(x,y)라면 x는 x끼리 연산하고 y는 y끼리 연산해주어야 했지만 float은 x,y 둘 다랑 연산해주면 됐거든요.

 

 

 

이를 그림으로 나타내면,

백터의 방향은 그대로지만 힘은 커졌다.

 

벡터의 스칼라 곱이란 벡터의 방향은 바꾸지 않은 채 힘(크기)만 변화시키는 것을 뜻합니다.

길이가 1인 벡터에게 0.5를 곱한다면 방향은 그대로고 길이만 줄어들겁니다.

 

단 음수(-)를 곱할 경우 방향은 반대로 뒤집어 집니다.

 

 

 

 

 

 

[ 2. 벡터의 내적 연산 ]

 

힘과 방향 둘 다 가지고 있는 친구들 끼리 곱할 땐....?

 

 

백터의 곱샘에는 내적(Dot) 연산과 외적(Cross) 연산 두 가지 유형이 있습니다.

 

이중 내적 연산(Dot Product)은 두 벡터의 각도를 나타내는 공식으로 Dot( · )으로 표기 합니다.

 

 

벡터의 내적을 구하는 방법으로는 2가지가 있는데요,

 

 

A(x¹,y¹) · B(x²,y²) = (x¹ * x¹) + (y² * y²) 

벡터의 모든 성분끼리 곱해서 더하는 방법

 

 

 

 

 

A · B = A의 길이 * B의 길이 * cosθ

위의 공식의 경우 정규화를 거치게 되면 Vertor의 값은 1이 되므로 궁극적으로 [ A · B = cosθ ]란 식이 도출됩니다.

 

먼저 설명한 예시에는 정규화를 거치지 않아 값이 다를 것이다. 정규화를 거친 뒤 값을 구한다면 같은 값이 나온다!

 

어찌 됐건 일단 내적 연산백터와 백터사이의 각도! 를 구한다는 것만 기억합시다!

복잡한 계산은 엔진이 해줄겁니다. 지금 단계에서 저 복잡한 공식을 외울 필요는 없습니다.

 

이러한 내적 연산은 많은 곳에서 쓰이는데, 그 중 하나가 바로 라이팅 연산입니다.

 

 

 

 

 

 

 

Normal

 

라이팅 연산을 이야기하기전 빼놓을 수 없는 것이 바로 Normal입니다.

Normal은 백터 그 자체라고 할 수 있는데요,

해당 오브젝트의 면, 또는 점이 가지고 있는 힘과 뱡향을 Normal이라 부릅니다. 한자어로는 법선.

 

 

 

Normal이 오브젝트에 빛을 받아오는 형식은 크게 두가지가 있습니다. 

 

 

 

 

  •  Face Normal

오브젝트의 면에 Normal Vector를 넣어 면단위로 그림자가 지도록 만드는 방식입니다.

Face Normal을 사용할 경우 각각의 면이 도드라져 보이며, 부드러운 블렌딩을 표현할 수 없었습니다. 

 

이러한 Face Normal의 단점을 보완하고자 나온 것이 바로 Vertex Normal입니다.

 

 

 

  • Vertex Normal

 

말 그대로 버텍스에 Normal을 박아넣고 버텍스와 버텍스 사이의 값이 보간되어 부드럽게 표현되도록 만든 방식입니다.

Face Nomal가 불가능했던 부드러운 표현이 가능했지만

 

이 방법의 문제점은 Face Normal처럼 딱딱하게 각지는 것을 표현할 수 없다는 것이었습니다.

http://wiki.polycount.com/wiki/VertexNormal

하나의 버텍스에는 기본적으로 하나의 노말값만 가질 수 있었기에,

같은 곳에 여러 개의 버텍스를 두는 무식한 방법으로 이 문제를 해결합니다.

 

결과적으로 스무딩과 하드 엣지를 함께 사용할 수 있게 되었죠.

 

 

 

이 말은 즉 모델링에 하드엣지를 넣었을 경우 해당 부분은 면의 수만큼 폴리곤이 더 늘어난다는 것을 의미합니다.

버텍스 하나 하나가 중요했던 예전엔 중요하게 다루는 시안이었지만,

그 때보다 하드웨어의 성능이 훨씬 좋아졌기에 그냥 알고만 있어도 괜찮은 부분입니다. 

 

 

 

 

 

 

내적 연산(Dot Product)과 Light

 

백터와 백터의 각도를 구하는 내적 연산은 라이트 연산의 기본입니다.

컴퓨터 그래픽스의 3가지 라이트들은 모두 벡터 값을 가지고 있는데요,

 

오브젝트의 버텍스에서 뻗어있는 노말 백터와 라이트 백터가 마주볼 때, 해당 버텍스는 가장 밝아집니다.

즉, 라이트 백터와 노말 백터의 각도 차가 0과 가까울 수록 해당 버텍스는 밝아지는 것.

 

하지만 라이트 백터와 노말벡터가 마주보는 상태로 연산을 하게되면 결과 값에 -가 붙어버리기 때문에

라이트 벡터에 -1을 곱해주어 두 백터의 기준점을 맞추고 내적 연산을 하여 두 벡터의 각도 차를 구하게 됩니다.

이를 노말(N)과 라이트(L)를 내적한다 하여 N·L, NdotL 이라고 부릅니다.

 

 

 

백터의 내적 연산은 코사인 그래프와 일치합니다.

해당 그래프를 살펴보면 각도에 따라 1에서 -1사이의 값을 가지고 있는 것을 알 수 있는데요,

노말 벡터와 라이트 벡터를 내적 연산 하였을 때 나오는 각도차만큼 1~-1사이의 값을 가지고,

이를 색으로 바꾸면 흰색에서 검은색이 됩니다. 

 

1 흰색
90˚ 0 검정색
180˚ -1 검정색

즉, 두 벡터의 각도 차이가 없을 수록 밝아지고, 커질 수록 어두워져 그림자가 드리워지는 것이죠.

 

 

내적 연산을 이용한 Lambert Light

 

 

 

 

 

커스텀 라이트

 

이제 지금까지 공부했던 이론과 공식들을 가지고 구형 라이트를 직접 만들어봅시다.

Lambert Light를 기반으로 점점 우리가 필요한 것들을 붙여나가며 '커스텀' 하는 것입니다.

이러한 구형 라이트는 호환성이 좋고 가벼워 지금도 많이 쓰이고 있습니다.

 

 

오늘 건들여 볼 친구.

 

처음 셰이더를 생성하여 코드를 열어보면 surface surf Standard라고 적혀 있습니다.

< Standard > 해당 여덟글자에는 물리기반 라이트, PBR의 코드가 다 들어있어가 있습니다.

이 코드를 쓴 다는 것은 그 연산들을 다 끌고 가는 것을 의미하기에 필요하지 않는 다면 사용하지 않는 것이 맞습니다.

 

과감하게 지워준 후 내가 원하는 이름을 붙여줍시다.

살....려...줘...

스피닛에서 Standard를 삭제하면서 void함수에서도 더이상 SurfaceOutputStandard 구조체를 불러올 수 없습니다. 

뒤에 Standard를 지워서 구형 라이트용 SurfaceOutput 구조체를 받아와줍니다.

 

SurfaceOutput 구조체에 들어있는 친구들.

 

하지만 저장해도 여전히 오류가 납니다. 해당 라이팅을 찾을 수 없다고 말이죠.

유니티에 내장되어있지 않고 제가 이름을 지어준 것이기 때문에 라이팅 모델을 직접 만들어주어야합니다.

 

 

이때, void함수를 사용하지 않고 float4함수를 사용합니다.

 

원래 모든 함수는 retrun값을 거치지만 void함수는 예외입니다. (선언만하고 못은 안 박는 느낌?)

void가 아닌 함수의 경우 반드시 특정 값을 반환해주어야 하기 때문에 return 값을 넣어주어야합니다.

 

위에서 뭘 했던 최종적으로 retrun에 선언된 값이 출력됩니다.

 커스텀 라이트 선언 뒤로 3개의 인자 값이 붙어있습니다. 이는 각각 어떤 역할을 수행하는지 알아봅시다.

 

 

 

 

  • [SurfaceOutput]

'표면 셰이더'로 여러가지 코드가 압축되어 들어있는 가방, 구조체입니다.

위에서 설명했던 것처럼 표준 출력 구조는 다음과 같습니다.

 

https://docs.unity3d.com/kr/2018.4/Manual/SL-SurfaceShaders.html

 

 

 

  • [LightDir]

https://forum.unity.com/threads/get-lightdir-in-vertex-shader.188928/

Surface Shader에서 라이트 벡터(Light Vector)를 확인하는 내장 변수 입니다.

적어주지 않으면 라이트 벡터를 불러올 수 없게 됩니다. 

 

이렇게 return값에 LightDir를 넣어주면 라이트 벡터를 색상으로 확인할 수 있습니다.

 

 

 

 

 

 

 

 

 

  • [atten]

atten은 라이트의 감쇠값으로 빛의 강도에 따라 라이트 감쇠 효과와 그림자를 표현할 수 있게 해주는 변수 입니다.

 

 

 

 

float4 (1, 0, 1, 1) 값을 출력하니 이렇게 뭔가 희뿌연... 색이 나옵니다. 이는 엔비언트가 출력되고 있기 때문인데요,

noambient를 적어 일단 엔비언트를 꺼줍니다.

 

오류난거 아님

 

 

 

현제의 코드는 어떠한 빛도, 엔비언트도 받지 않아 그야말로 본연의 색이 나오는 상태입니다.

여기에 텍스쳐를 출력해 준다면

 

 

어떠한 빛의 영향도 받지 않는 가장 가벼운 쉐이더. Unlit Shadar가 완성됩니다.

이펙트를 만들 때 주로 쓰이는 쉐이더죠. (nofog까지 해주면 더 좋아용)

 

 

여기서 한가지 특이한게 Emission입니다. 

void 함수에서 Emission을 사용할 경우 코드상엔 보이지 않지만 코드 맨 마지막에 Emission을 한 번 더 더해주게 되는데요.

 

즉,

검정색을 출력하여도
텍스쳐가 출력됩니다.

 

 

만약 return값에도 Emission을 출력하게 되면 어떻게 될까요?

 

더 밝아져 버렸다.

Emission을 한 번 출력하고 거기에 한 번 더 Emission이 더해져 더 밝아져버리고 말았습니다.

 

 

 

틀을 만들어 줬으니 본격적으로 라이트 연산을 해봅시다.

 

Dot 연산은 다음과 같이 표기합니다.

dot(A,B)

NdotL의 경우 각각의 위치에 Normal vector와 Light vector를 넣어주면 되는데요,

각각 s.Normal, lightDir로 불러올 수 있습니다.

 

Normal의 경우 따로 선언하지 않으면 기본적으로 버텍스 노말이 불러와진다.
우효~~~~

여기에 텍스쳐만 곱해주면 끝...일 것 같지만 이러한 내적연산은 마이너스(-)값까지 가지고 있습니다.

코사인 그래프에서 180˚는 -1의 값을 가지고 있었죠.

Light Vector와 정 반대의 방향의 Normal Vector를 가지고 있는 버텍스는 -1의 값을 가지게 됩니다. 

추후 연산을 할 때 문제가 될 수 있다는 것. 

 

 

이렇게 0과 1사이를 초과해버리는 값을 Saturate를 이용하여 0과 1 사이를 벗어나지 않도록 딱딱 잘라줍니다. 

-1의 유무는 엔비언트를 넣어보면 쉽게 육안으로 확인할 수 있습니다.

 

음수 값이 존재할 땐 엔비언트가 정상적으로 들어가지 않는다.

 

 

하지만 지금 상태는 되는 것보다 안되는 것이 더 많습니다.

 

텍스쳐는 NdotL이 float이니 Albedo를 불러와 NdotL을 곱하여 합쳐줄 수 있습니다.

Albedo만 불러왔을 땐 아무 그림자 없이 밝았는데 NdotL을 곱하는 순간 그림자가 생겨 어두워 졌습니다.

 

여기에서 < 빛 연산은 밝아지게 만드는 것이 아닌 '그림자를 만드는 것'>임을 알수 있죠. 그래서 'Shader'입니다.

 

똑같다...... (ambient는 다시 꺼줬다)

lightDir는 단순히 라이트의 방향만 가져오기 때문에 디렉셔널 라이트의 색상과 강도가 적용되지 않으며,

사물에 의한 그림자도 적용되지 않습니다. 코 밑이나 목 밑에 드리워져야할 그림자가 없는 걸 확인할 수 있죠.

 

 

이 두가지를 불러와 주는 것이 _LightColor0atten 입니다.

새로운 변수를 선언하여 코드를 정리하면서 이 친구들을 모두 적용해봅시다.

 

Alpha는 사용하지 않아도 선언해주는 것이 예의라고 합니다.

엠비언트도 켜주고 디렉셔널 라이트의 색상과 강도를 조절해주니

Specular도 없는 구형 라이트라는 것이 믿기지 않을 정도로 꽤 많이 예쁩니다.

해질녁 강한 햇빛을 받는 느낌이네요...

 

이렇게 만든 쉐이더는 유니티에 내장되어있는 레거시 쉐이더 중 Diffuse Shader와 정확하게 일치합니다.

 

 

 

 

 

 

 

하프 램버트 (Half Lambert)

 

Lambert light를 보고 있으면 한가지 조금 아쉬운 점이 있습니다. 바로 음영이 좀 극단적으로 보인다는 것...

현실 세계에서는 수많은 반사광으로 인해 조명과 정 반대에 있는 면도 완전히 까맣게 되지는 않습니다.

하지만 이걸 실제로 계산하려면 그 때의 하드웨어에겐 너무 복잡하고 무거운 연산이 들어가야했습니다. 

 

 

Half Lambert Shader는 밸브가 하프라이프를 개발하던 당시 공개했던 공식으로,

기존의 Lambert light를 개량하여 더 부드럽게 음영처리가 되도록 만들어

마치 주변의 반사광을 받은 것처럼 보이게 하는 효과를 가져왔습니다.

 

 

그 마법의 공식은 바로 [ * 0.5 + 0.5  ]

 

처음 보다 훨씬 부드러워졌다.

이러한 방법은 물리적으로 옳진 않습니다.

하지만 예쁘죠. 쉐이더의 근본적인 목표입니다. 물리적으로 맞는가는 그 뒤의 문제. 일단 예뻐야합니다.

 

 

이 하프 렘버트 공식을 코드에 직접 넣어봅시다.

하프 렘버트 공식을 이용하기 위해선 음수값이 필요하기 때문에 먼저 saturate를 주석처리 해줍시다.

 

 

 

 

 

 

 

그런데 하프 렘버트가 생각보다 많이 부드럽게 만들기 때문에...  atten을 적용했을 때 좀 어색해집니다.

무식한 해결 방법으로는 하프 렘버트를 제곱하여 음영을 진하게 만들어 버리는 것.

 

 

진짜 무식하게 곱했지만

 

이렇게 하는게 더 가볍다!