FX/Houdini_Joy of VEX

Joy of Vex - Day 9-2: dot and cross product, fake lighting combing normals to a surface, vector maths primer

Gamestonk118 2022. 7. 6. 19:29

중요했던 부분:

- 지난번에 내용한 것 복습

normalize() - 크기 1짜리 vector로 만드는 과정

dot() - 각도를 알려주는 Function. 빛이 물체에 닿았을때 normal과 그 포인트가 광원을 향할때의 각도. 어떤부분은 밝게 아님 어둡게를 지정함

cross() - 두 vector의 동시에 수직이 되는 vector

vector의 연산 - 방향을 정해줄때 목표지점에서 시작점을 빼줘야 원하는 방향을 구해주는 vector연산이 된다

 

- dot()의 활용

dot() 즉 내적으로 구한 각도값으로 light과 geometry가 있을때 빛이 어느 방향으로 쐬어지는지 @Cd와 같은 색깔값으로 나타낼 수 있다.

@Cd = @N.y; // 위에서 geometry에 빛을 직빵으로 내리꽂는 느낌

@Cd = @N.y를 Tommy에 적용하였을때의 색깔 변화. 라이팅을 따로 생성해주지 않았어도 마치 빛이 위에서 내리때리는 듯한 느낌으로 색상을 지정해 줄 수 있다.

@Cd = -@N.y; // 아래에서 geometry에 직빵으로 올려꽂는 느낌

@Cd = -@N.y를 Tommy에 적용하였을때의 색깔 변화. 라이팅을 따로 진행해주지 않았어도 마치 빛이 아래에서 올려때리는 듯한 느낌으로 색상을 지정해 줄 수 있다.

이러한 원리를 이용하여 dot()을 활용하여 각도를 지정해주면 빛이 퍼지는 범위와 밝기를 @Cd 색깔값으로 조절해 줄 수 있다.

dot(A, B)과 같은 경우는 |A|*|B|*cos(θ)과 같은 원리로 작동되며 이때 |A|와 |B|는 크기가 1로 normalize()화가 되있어야 한다.

dot (A, B)

= length (A)*length (B)*cos (θ) // A와 B가 normalize()되어 length()가 적용된 값이 1이여야 한다

= cos (θ)

dot()의 기본 내적 방식 원리

우선적으로 이렇게 모양을 꾸며 왼쪽의 포인트는 광원, 그리고 오른쪽 geometry는 반사되는 물체 이렇게 표현하기로 하였다.

위 모양에서 wrangle node들과 코드를 통해 왼쪽 포인트에서 부터 geometry를 빛으로 비추면서 나타나는 스타일의 색상을 맞추려고 하였다.
이런식으로 노드를 구성하여 윗 wrange에는 circle의 normal값을 지정해주는것으로 초점을 맞췄고 아랫 wrangle에는 circle의 포인트들과 add의 포인트를 불러와 각기 알맞는 vector들을 계산해서 만든 각도들을 조절하여 circle에 point로 인한 빛의 반사 효과를 유사하게 주도록 유도하였다.

// 첫번째 wrangle node에 들어갈 코드

/* 원의 base를 직접 유저가 파라미터로써 조절할 수 있게끔 하여라. 이는 circle노드의 center값을 
chv()로 생성한 baser값으로 copy parameter 함으로써 조절 가능케 하라*/
vector base = chv ("BASE"); 
// circle의 모든 point에서 base vector를 뺀값으로 하여 base로부터 포인트의 방향으로 향하는 vector값을 생기게 해라
// 이는 vector의 방향은 target - base (목적점 - 시작점)으로 구해줄 수 있으므로
// 그 값을 모두 vector variable temp에 저장
vector temp = @P - base;
// vector temp값의 "크기"를 1로 업데이트 하여라
temp = normalize (temp);
@N = temp; // temp값을 @N (Normal)에 저장하여 노멀값으로써 나타나게끔 하여라
// 두번째 wrangle 노드에 들어갈 코드

/* 1번 input에 연결된 geometry 혹은 attribute node의 0번 포인트의 @P (포인트 위치)값을 가져와 vector
variable sun에 저장하여라*/
vector sun = point (1, "P", 0);*/
/* sun의 위치에서 모든 포인트를 뺀값 (목적지 - 각각의 위치) 즉 모든 포인트로부터 왼쪽 점의 값. 즉 
방금 선언한 vector variable sun의 포인트 위치값까지의 방향을 구하여라*/
vector dir = sun - @P;
// 모든 vector값의 길이를 1로 바꾸어버려라
vector nDir = normalize (dir);
/* 방금얻은 normalize()가 적용된 (크기가 1인) 모든 포인트에서로부터 sun의 위치로 향한 방향 vector값과
circle의 @N (노멀값) 사이의 내적 각도를 계산하여 float variable howMuch에 집어넣어라*/
float howMuch = dot (nDir, @N);
f@howMuch = howMuch; /* 방금전 선언한 float variable howMuch (각도값)을 attribute로 
geometry spreadsheet에 디스플레이하여라*/
@Cd = howMuch; /* 방금전 선언한 float variable howMuch (각도값)을 0번 인풋으로 들어간 geometry (circle)의 
색깔값으로 지정하여 마치 왼쪽 포인트가 오른쪽 circle표면에 빛을 쬐는것처럼 연출하여라. 빛이 닿는 부분일수록 
색이 밝아보여 이러한 현상을 가능케함*/

방금전 이미지와는 다르게 circle의 왼쪽 부분이 포인트와 방향이 맞닿아 있으므로 윗 코드를 통해 밝게 나타나진걸 볼수 있고 반대쪽은 어둡게 나타나진것을 볼수 있다.

이는 아래 이미지의 원리로 설명되며 포인트를 향하는 벡터와 geometry의 normal의 벡터의 내적 각도가 적어질수록 더욱 cosine파형의 y값 수치가 올라 더욱 밝은 색을 낼것이고 내적 각도가 커질수록 cosine파형의 y값 수치가 작아져 더욱 어두운 색을 낼 것 이다.

윗 이미지를 설명하는 식. 이 이미지와 동일하게 위 이미지도 포인트를 향하는 vector방향과 normal의 방향의 각도가 줄수록 더욱 빛의 intensity가 세지는것을 알 수있다.

dot()을 통해 각도에 해당하는 θ값을 구할 수 있으며 dot()을 포함하는 howMuch라는 float variable howMuch자체에 식을 더 적용해줌으로써 cos()파형에 변화를 주어 각기 빛이 퍼지는 범위와 빛의 밝기(세기)를 조절할 수 있다.

// cos() 파형에 상수를 더해준다면

float howMuch = dot (nDir, @N); // dot()이 적용된 vector값들을 모두 포함하는 float variable howMuch
@Cd = howMuch + chf ("ADD"); // 방금전 선언한 variable howMuch에 상수를 더해준다면...

빨간파형 - 원래 파형, 파란파형 - 상수를 더해준 파형. y-axis을 기준으로 0아래 있는 x값 수치들을 모두 어두운색으로 나타나진다 했을때 상수를 더해졌을때 파형 자체가 위로 올라가 y-axis을 기준 0아래 있는 x값 수치들이 적어져 더욱 밝은색의 범위가 geometry에서 늘어난다라고 볼 수 있다.

// cos() 파형에 상수를 곱해준다면

float howMuch = dot (nDir, @N); // dot()이 적용된 vector값들을 모두 포함하는 float variable howMuch
@Cd = howMuch*chf ("MULT"); // 방금전 선언한 variable howMuch에 상수를 곱해준다면...

빨간파형 - 원래 파형, 초록파형 - 상수를 곱해준 파형. y값을 기준으로 0아래 있는 x값 수치들을 모두 어두운색으로 나타나진다 했을때 상수를 더해졌을때 파형의 높이가 커져 y-axis를 기준 0아래 있는 x값들의 y값은 더 작아지고 0위에 있는 x값들의 y값은 더 커져 geometry의 밝은 색이 더 밝아지고 어두운 색은 더 어두어진다고 볼 수 있다

즉 위와같은 개념들을 적용하여 이러한 식을 배치함으로써 아래와 같이 식을 쓸 수 있다.

@Cd = howMuch + chf ("ADD"); // 빛이 퍼지는 범위를 조절할 수 있음
@Cd = howMuch*chf ("MULT"); // 빛의 세기 (밝기)를 조절할 수 있음

// 위 식들을 하나의 식으로 합쳐서 이렇게 식을 적용킬 수 있다
howMuch = (howMuch + chf ("ADD"))*chf ("MULT");
@Cd = howMuch;

위의 현상을 광원을 두 개를 불러와서 동시에 진행 시킬수도 있다.

/* point()를 활용하여 각각의 wrangle node input에 연결된 지오메트리의 point를 가져와 
vector variable al과 bl에 저장*/
vector al = point (1, "P", 0);
vector bl = point (2, "P", 0);
// 모든 포인트로부터 각각의 광원으로 향하는 방향을 지정해줌 (목적점 - 시작점)
vector ad = al - @P;
vector bd = bl - @P;
// 각기 향하는 방향 vector값의 크기를 1로 바꾸어줌
vector nAd = normalize (ad);
vector nBd = normalize (bd);
/* 0번 인풋에 연결된 geometry의 @N과 모든 포인트로부터 각기 광원으로 흐르는 방향의 vector값 사이의 
내적각도를 구하여라 */
float av = dot (nAd, @N);
float bv = dot (nBd, @N);
// dot()이 포함된 float variable들 자체에 상수를 더해주고 곱해줌으로써 빛이 퍼지는 범위와 밝기를 조정할수 있게하라
av = (av + chf ("ADD_A"))*chf ("MULT_A");
bv = (bv = chf ("ADD_B"))*chf ("MULT_B");
// geometry의 @Cd를 rgb값으로 따로 조절 가능케하여 색깔로서 빛을 더하여라
@Cd = set (av, av/2 + bv/2, bv);

위와 같은 식으로 두개의 광원이 적용이된 동시에 빛이 색깔로써 나타나게끔 연출 할 수 있다

또한 광원을 tommy에다 갖다대므로써 보다 입체적으로 빛을 가져다 줄 수 있는지 실험하였다.

*이때 처음에 실험한 코드로써는 광원의 위치에서부터 tommy의 모든 포인트들의 위치값들을 빼주어 방향설정을 하지 않고 바로 노멀값과 내적 각도를 생성해주어 원점으로부터 광원을 향한 vector의 방향과 normal값들에 기반한 색깔이 나오게 되었다. 

// 1번 인풋으로 연결된 geometry의 0번 포인트의 @P값을 가져와 vector variable pos에 저장
vector pos = point (1, "P", 0);
// 앞서 선언된 vector variable pos 자체를 normalize()하여 크기를 1로 바꿈
pos = normalize (pos);
/* 그 값을 바로 dot()에 적용하여 원점으로부터 광원을 향한 vector의 방향과 normal값들의 
내적각도에 기반한 색깔이 나오게 되었다. */
@Cd = dot (@N, pos);

tommy의 모든포인트로부터 광원을 향한 vector 방향값들을 설정해주지 않아 원점으로부터 광원까지의 방향만 나타나 있는 것을 알 수 있다.
이는 마치 아주 먼 태양빛이 한 방향으로 내려오는 느낌이 들게끔 만든다.

// 1번 인풋으로 연결된 geometry의 0번 포인트의 @P값을 가져와 vector variable pos에 저장
vector pos = point (1, "P", 0);
// geometry의 모든 포인트들로부터 광원까지 퍼져나가는 방향의 vector값을 vector variable dir로 저장함
vector dir = pos - @P;
// dir값을 normalize()하여 크기를 1로써 만들어두어라
vector normDir = normalize (normDir);
/* 방금전 모든 포인트들로부터 광원까지의 vector와 geometry의 normal값의 내적 각도를 구하여 색깔값으로 지정하여라*/
@Cd = dot (@N, normDir);

tommy의 모든포인트로부터 광원을 향한 vector 방향값들을 설정해주어 tommy의 모든 포인트로부터 광원까지의 방향들이 모두 나타나 있는 것을 알 수 있다.
가까이 있는 불빛이 사방으로 뻗어나가 tommy의 정면이 더욱 밝아보이는 효과를 줄 수 있다.

 

- cross()의 활용

cross() 즉 두 vector값의 수직 vector값을 구함으로써 geometry의 @N값을 수동으로 구할 수 있다.

cross()의 원리. 여기서 핵심은 A와 B 즉 인풋으로 들어간 vector값들로 평행사변형을 만들었을때의 넓이가 length (c) 즉 vector C까지의 길이값으로 적용된다.

 

만약 평면적으로 이렇게 크로스된 두개의 vector값이 있다고 가정해보자

끝의 숫자들은 각각의 포인트 넘버들을 의미한다. 이 두 vector들은 평면적으로 누워져 있다고 생각해보자.

위에 세팅한 vector값을 통해 cross()의 결과를 나타내기 위해서 vex를 작성하였다.

// cross()로 생성된 point를 오직 한번만 생성시키기 위하여 run over를 detail로 바꾼다

// 방금 전 만든 모든 점들의 위치정보를 불러와 각기 vector variables들에게 저장하여라
vector a = point (1, "P", 0);
vector b = point (1, "P", 1);
vector c = point (1, "P", 2);
vector d = point (1, "P", 3);
// 위 이미지와 동일하게끔 방향값을 vector값으로 돌려주어라 (목적점 - 시작점)
vector A = b - a;
vector B = c - d;
// 위의 방향이 설정되있는 vector variables을 geometry spreadsheet에 디스플레이
v@A = A;
v@B = B;
// cross()를 통해 위의 vector값의 수직의 방향인 vector값을 result로 저장시킨다
vector result = cross (A, B);
addpoint (0, result); // cross()로 생긴 vector 위치에 포인트를 생성한다

// 윗 wrangle node에 다른 wrangle node를 이어붙여 길이값을 알 수 있다. 
f@d = length (@P); // 0번 포인트에 연결된 wrangle 노드의 모든 포인트의 대해 원점으로부터 길이값을 구하라

이와 같이 생성하면 오른손 법칙을 통해 cross()로 인한 포인트가 어느 방향으로 생성될지 예측할 수 있는데 이는 cross()안 A (검지) 다음 B (중지) 순서임으로 포인트는 아래에 생성되는것을 유추할 수 있다.

 

또한 이와같이 vector값이 정해졌을때 특정한 방향으로 뻗친 cross() 결과 vector값을 유추할 수 있는데

@N = cross (@N, {0, 1, 0}); // 노멀값과 위로 향하는 vector값으로 이루어진 수직 방향의 vector값을 구하여라

검은색은 @N, 빨간색은 {0, 1, 0} vector값. 그러므로 cross()로 생성된 결과값으로써 파란색과 같은 방향으로 나타나지어는것을 알 수 있다.

아까 만들어둔 cross()값을 vector variable temp로 지정해주어
이렇게 이미 만들어진 cross()결과값을 인풋으로 사용하여 또 cross()을 적용시키면 새로운 수직방향을 만들어내
normal방향이 계속 돌아가게끔 만들어 줄 수 있다. 오른손 법칙을 보면 세 손가락끼리 계속 상호작용함으로.

vector temp = cross (@N, {0, 1, 0});
//이는 총 네번을 반복하여 다시 원점으로 돌아가게 하는 결과이다.
temp = cross (@N, temp);
temp = cross (@N, temp);
temp = cross (@N, temp);
temp = cross (@N, temp);
@N = cross (@N, temp);

위와 같은 식을 sphere에 적용하면 이러한 normal의 생성을 볼 수 있다.

 

- vector값의 계산

vector값들을 여러가지 방식으로 계산해주어 더욱 다양한 normal값들을 생성해 줄 수 있다.

 // chv()값을 통해 vector값을 파라미터로 조정할 수 있는 variables a 와 b를 생성
vector a = chv ("A");
vector b = chv ("B"); 
@N = a + b; // a와 b즉 두 Vector의 합이 @N의 값으로 지정됨

// 마찬가지로 빼준다면 목적점 - 시작점으로 시작점에서 목적지로 향하는 방향의 노멀값을 생성시킬수 있음
@N = b - a;

// Normal 자체에 vector값을 더해주면 기존에 있던 Normal의 방향에 변화를 줄 수 있다
@N = @N + chv ("DIRECTION");

// 마찬가지로 Normal자체에 각기 vector파라미터를 곱해줌으로써 각 Normal 크기를 마음대로 조절할 수 있다.
@N = @N*chv ("SCALE");

@N에 vector을 곱해주어 어느 방향으로 사이즈를 키워줄지 지정할 수 있다.

이렇게 모든 포인트에 대해 각기 한 포인트를 바라보고 하게 싶으면 이런 식을 적용할 수 도 있다.

문제는 run over가 point인 상황에서 모든 포인트가 한점만 특정하게 잡기 위해선 @ptnum을 사용해야 한다.

0번 포인트를 가진 a라는 변수는 0번 포인트로부터 시작된 vector값을 갖는다. 4번 포인트를 가진 a라는 변수는 4번 포인트로부터 시작된 vector값을 갖는다. 그러므로 각각에 알맞는 포인트들의 넘버를 지정해주기 위해 (시작점이 다 다르므로) point()에 @ptnum을 넣어 각각의 @ptnum에 고유한 포인트 위치를 시작점으로 만들어준다. 

int pt = @ptnum; // @ptnum (point number)를 integer variable pt에 저장하여라
// 0번 인풋으로 연결된 geometry의 각기 포인트 넘버값의 @P (포인트 위치)를 불러와 vector variable a에 저장
vector a = point (0, "P", pt);
// 0번 인풋으로 연결된 geometry의 0번 포인트의 @P (포인트 위치)를 불러와 vector variable b에 저장
vector b = point (0, "P", 0);
// a vector로부터 b vector까지의 방향을 @N으로 지정하여라
@N = b - a;

윗 코드를 활용하여 모든 포인트로부터 한 포인트를 바라보게끔 normal값들을 세팅할 수 있다.

- Color (Color)

연결된 노드의 색을 지정해 주는 노드

 

- Display Options (d) -> Geometry -> Wireframe -> Wire Width

Wireframe의 선 굵기 값을 정해줌

 

- Detail Geometry Spreadsheet

Detail Wrangle상에서 입력된 결과값들을 확인 할 수 있다.

이런식으로 나와있는 것을 알 수 있다. 각 variable들의 vector값들을 알려준다.

- Normal (Construct -> Add Normals To)

Normal을 Scene View에서 디스플레이 할때 point/primitive/vertex/detail중 어디를 기준으로 할지 정해주는 노드

 

Exercises:

1. Cross against @N and noise, what happens?

cross()를 사용하여 한 쪽 인풋에는 @N 그리고 다른쪽 인풋에는 noise(@P)값을 넣어 어느 방향으로 normal이 생성될지 실험하였다.

vector noise1 = noise (@P); // @P 포인트 위치에 noise를 적용하여 vector variable noise1에 저장
vector temp = cross (@N, noise1); // @N과 noise1의 방향에 대해 cross product하여
@N = temp; // 새로운 normal값으로 나타내어라

이는 grid에서 기본적으로 위로 퍼져나가는 @N값 그리고 원점으로부터의 @P값이었는데 이 @P값에 noise를 적용하여 cross()를 하니 일정한 방향으로 향하는 normal값들을 볼 수 있었다. 

우측 위로 뻐져나가는 normal의 방향

2. double cross @N and noise?

vector noise1 = noise (@P); // @P 포인트 위치에 noise를 적용하여 vector variable noise1에 저장
vector temp = cross (@N, noise1); // @N과 noise1의 방향에 대해 cross product하는데
temp = cross (@N, temp); // 여기서 생성된 normal을 또 이용하여 cross product하여
@N = temp; // 새로운 normal값으로 나타내어라

cross()를 두번 적용해주었더니 똑같은 모양이지만 방향이 좌측으로 꺾인 normal값들을 생성해 낼 수 있었다. 이는 위에 배운 cross()를 이미 나온 결과 vector값에 또 해주면 방향만 바뀌면서 반복이 된다는 사실을 일깨워주는 문제였다. 

좌측 위로 뻐져나가는 normal의 방향

3. Against @P?

vector noise1 = noise (@P); // @P 포인트 위치에 noise를 적용하여 vector variable noise1에 저장
vector temp = cross (@N, noise1); // @N과 원점으로부터 @P의 방향에 대해 cross product하여
@N = temp; // 새로운 normal값으로 나타내어라

이는 윗 문제들보다 조금 더 직관적으로 알 수 있었다. grid의 @N의 방향은 쉽게 알 수 있었지만 @P는 어떤기준으로 vector값이 지정이 될까 했는데 단순히 원점으로부터 point위치까지의 방향이 vector값이었다. 그말은 오른손 규칙을 이용하여 손쉽게 cross()된 결과 normal 방향을 유추할 수 있었다. 이는 모든 포인트들의 대하여 cross()됨으로 회오리같은 모양을 나타냈다.

회오리처럼 감기는 normal의 방향

4. Can you fix the colours going negative in the shadowed areas of the dot product examples? (Hint: You'll need to clamp colours to stay within a valid range)

힌트에 나와있듯이 간단하게 clamp()을 사용하여 그림자 비치는 부분을 최소 @Cd 색깔값 0으로 설정하여 더이상 마이너스의 구간으로 내려가지 않도록 하였다. 이는 더욱 음영부분을 밝게 만들어 전체적으로의 geometry의 밝기를 조정하였다. 

// 1번 인풋으로 연결된 geometry의 0번 포인트의 @P값을 가져와 vector variable pos에 저장
vector pos = point (1, "P", 0);
// geometry의 모든 포인트들로부터 광원까지 퍼져나가는 방향의 vector값을 vector variable dir로 저장함
vector dir = pos - @P;
// dir값을 normalize()하여 크기를 1로써 만들어두어라
vector normDir = normalize (normDir);
/* 방금전 모든 포인트들로부터 광원까지의 vector와 geometry의 normal값의 내적 각도를 구하여 
vector variable color값으로 저장하여라*/
// dot()이 적용된 @N값과 광원을 향한 normDir값의 내적 각도를 구하여 vector variable color에 저장
vector color = dot (@N, normDir);
/* clamp()을 활용하여 color값의 Range를 0과 각기 geometry point들에 할당된 color값으로 
지정해주면서 @Cd값이 음수로 내려가지 않도록한다*/
@Cd = clamp (color, 0, color);

윗 이미지와 비교하면 tommy의 음영이 한층 더 밝아진 것을 볼 수 있다.

 

이해가 안되었던 부분:

- 위에 첫번째와 두번째 exercise에서 cross()를 @N과 noise를 사용하여 새로운 Normal을 생성할때 어떤 방식으로 normal이 그런 방향으로 생성되었는지 이해가 잘 되지 않았다. noise()의 방향은 많이 랜덤하게 지정되어있는데 어떠한 과정을 통해 한쪽 방향으로 Normal이 쏠리는지에대해 정확한 이유는 내 머리로는 한계에 왔던것 같다. noise(@P)를 하면 정확히 어떠한 방향으로 vector가 지정되는지 그런 흐름이 있는지에대해 복습할때 다시 한 번 살펴봐야 겠다고 느꼈다. 그 문제만 파악하면 만능의 오른손 법칙을 이용하여 어떠한 방향으로 cross()결과 vector값이 나타날지 유추할 수 있기 때문이다. 

 

공부하면서 들었던 생각:

이번 강의는 어렵다기보단 필기 할게 많았던 것 같았다. 솔직히 이런 필기를 할때마다 굳이 이렇게 까지 해야하나라는 생각도 들곤한다. 근데 이렇게 안하면 뭔가 나중에 까먹을것 같다. 이번 Day9은 어제에 이어서 정말 컴퓨터 공학을 배우는 것 같았다. 지금까지 3D 툴을 이용했을때 lighting을 만드는 과정이 이러한 방식으로 생성되는구나라는 생각을 들게하고 개발자들의 존경심이 더욱 커진것 같다. 이번 강의를 통해 후디니 뿐만이 아닌 전체적인 3D의 Lighting의 원리를 조금이라도 배운것 같아 기분이 좋다. 학창시절에 배웠던 공식들과 수학적인 원리들이 이렇게 3D에 접목된다는게 참으로 신기하기도 한것 같다. 그리고 Day1때 TWA님이 제 이해안가는 점을 언급해주셨는데 이번 강의를 통해 normal에 방식과 중요성 그리고 응용을 완벽하게 설명해주신것 같아 후련한 느낌이 든다. 이제 점점 후디니에 인생을 걸어가는 느낌이 조금이지만 생기는 것 같다. 처음에 어렵게만 느껴졌던 FX가 점점 내 곁으로 친해지는 느낌이 들어 신기하기도 하다. 오늘 꿈에는 grid가 일렁이는 꿈도 꾸었던것 같다. 하지만 아직은 부족하다고 느낀다. 365일 24시간 vfx를 생각하는 내가 되고 싶다. 필기가 많았지만 확실하게 공부할 수 있었던 오늘 강의였다.