일/Data Mining

[퍼옴] 벡터의 내적과 외적

LEEHK 2007. 4. 5. 00:07
벡터의 내적과 외적, 법선벡터를 알자.

먼저 벡터의 내적과 외적을 알기전에 벡터에 대해 조금만 얘기하겠습니다.
당연히 수학적인 부분이지만 초등학생도 알수 있도록 쉽게....


1. 벡터

2차원 좌표상에 점을 표시할때 일반적으로 x,y 두개의 좌표를 가지고
화면의 점을 그린다. 이때 수학적으로 점이란 눈에 안보이는 것이지만
점을 구성하는 좌표 성분으로 P(x,y)라고 지정한다.

벡터란 원점을 기준으로한 점이라고 생각하면 쉽게 설명할 수 있을 것
이다. V(x,y)를 표시할때 결국 (0,0)에서 (x,y)의 방향을 가르키는
말이며 v(2,2)와 v(3,3)은 결국 크기만 다르지 같은 방향을 가르키고
있다.


2. 단위 벡터의 특성

단위 벡터란 크기가 1인 벡터를 얘기한다.

0 에서 1까지의 실수는 아무리 곱해서 절대로 1을 넘지 않는다.
이 특성이 단위 벡터에서도 나타난다.
단위 벡터끼리 곱하는 연산은 1000만번을 한다하더라도 단위 벡터다.
사실 위의 예기는 아주 중요한 얘기이며 이 간단한 사실만으로
연산을 아주 간소화 할수 있다.

그럼, 단위 벡터는 어떻게 만드는가?
v(1,1)와 v(2,2)는 크기가 다르지만 방향을 같다고 했다.
두 벡터를 크기는 무시하고 오직 방향만 계산하고 싶다고 할때
단위 벡터를 만든다. 결국 크기는 1이니까..

같은수에다 같은수를 나누어 보라..
예를 들어 8 / 8 = 1, 7676 / 7676 = 1
역시 1이다.

단위 벡터도 이렇게 구한다.

벡터 v(x,y)가 있을때 벡터의 크기는 sqrt(x*x + y*y)이다.
그럼, 이걸로 나누면 땡이다.

v = sqrt(x*x + y*y);
vx /= v ;
vy /= v ;

이제 벡터의 크기를 구해보자..

크기 = sqrt(x*x + y*y)는 1이 나와야 한다.

이것은 영어로 Normalize라고 한다.

3. 벡터의 내적

이제 좀 어려운 부분을 얘기 하겠습니다.
벡터의 내적은 꼴도보기 싫은 수학정석에 나와 있습니다.
영어로 DotProduct 혹은 inner-Product, Scalar-Product라고 하더군요..
벡터의 내적 공식은 두 벡터가 있을때 두 벡터 사이의 각도를
구하는 공식이죠..

그럼.. 꼴도보기 싫은 수학정석에

cos(theta) = a*b / |a|*|b| 라고 되어 있습니다.

(참고) *는 곱셈이 아니고 . 이지만... 표기할게 없어서..

공식이 어떻게 나왔나고 묻지는 마세요.. 책에 그렇게 되어 있기에..

위 공식에서 만일 a와 b가 단위 벡터라면 |a|*|b|는 1이겠지요..

그럼.. cos(theta)= a*b로 간략화 營윱求

a*b 벡터의 연산은 성분끼리 곱하면 됩니다.

cos(theta) = a_x*b_x + a_y*b_y ;

(예)

float a_x, a_y, b_x, b_y;
float v, costheta, theta;

a_x = 1.0; a_y = 3.0;
b_x = 3.0; b_y = 1.0;

// 먼저 단위 벡터로 만든다.
v = sqrt(a_x * a_x + a_y * a_y);
a_x /= v;
a_y /= v;
v = sqrt(b_x * b_x + b_y * b_y);
b_x /= v;
b_y /= v;

theta = a_x*b_x + a_y*b_y ;
costheta = cos(theta);

음.. 이제 costheta를 구했다면 황당한 값이 나옵니다.

이걸가지고 어떻게 하란 말야..

cos(theta) = rad 라고 했을때

theta = acos(rad) 이렇게 역으로 구할수 있습니다.

그럼 위의 프로그램에 더 첨가합니다.

theta = acos(costheta);

결국 우리가 바라는 벡터 사이의 각이 나왔습니다.

신기하져?

(참고) 라디안 값
일반적으로 sin, cos, tan 함수에서 sin(theta) = rad 일
theta = asin(rad) 이렇게 역함수가 존재 합니다.
tan는 atan 흔히들 아크 함수라고 하져..

그리고 각도를 얘기할때 0~360도 얘기하는 것은 Degrees 값이라고
하며 수학에서는 보통 Radian 값을 씁니다.

0에서 180까지의 Degree 값을 얘기할때 라디안 값은
0에서 3.141592(즉 PI) 값까지 나오지요..

180 : 3.141592 = degree : rad 이렇게 되지요..

rad = degree * 3.141592654f / 180 ;
degree = rad * 180 / 3.141592654f ;


그럼. 다시 벡터를 얘기합니다.

theta = acos(costheta); 에서 나온 값은 라디안 값이므로
degree = theta* 180 / 3.141592654f ;

해보면 일반적인 각도가 나옵니다.

4. 벡터의 외적

벡터의 외적이 꼴도 보기 싫은 고등학교 수학 정석에 있었는지는
잘 모르겠네요.. 뒷장까지 본적이 없어서..

벡터의 외적은 그럼 무엇일까요? 벡터 사이의 각이 아닌
반대 방향 각을 구하는 공식일까요? 아닙니다.
벡터의 내적과는 성격이 좀 다릅니다.
벡터의 내적은 결국 라디안 실수 값이 나오지만
외적을 구하는 공식은 그냥 벡터가 하나 더 생깁니다.

두개의 벡터가 있을 기준점에 수직으로 못을 하나 꽂으면
못 방향으로 벡터가 하나 생깁니다. 두 벡터에 수직인 벡터
가 하나 더 생기는 셈이지요..
영어로 CrossProduct라고 하져..

그림으로 설명하면 더 쉬운데... 쩝..

v1(x,y,z)와 v2(x,y,z)가 있을 (0,0,0)을 출발점으로 한 위로
우뚝선 벡터 n(x,y,z)가 하나 더 생긴단 말이져..

두벡터에 수직인 벡터는 사실 두게 있습니다.
위아래...

보통 시계 방향이냐 반시계 방향이나 따라서 한가지만 뽑아냅니다.

다음은 내적을 구하는 연산입니다.
외우지는 마세요.. 그냥 베껴 쓰면 되니깐...^^;

n_x = v1_y * v2_z - v1_z * v2_y;
n_y = v1_z * v2_x - v1_x * v2_z;
n_z = v1_x * v2_y - v1_y * v2_x;

주의 하실점은 반드시 벡터를 단위벡터로 만들고 하세용..


5. 법선 벡터

법선 벡터는 3D 그래픽 프로그래밍에서 흔히 노말 벡터라고 합니다.
3차원 상에 점(vertex)가 3개가 있다고 합시다.
그럼.. 3개의 버텍스 사이에 면이 생깁니다. 일종의 평면이지요..
이 노말 벡터는 의 앞뒤를 가르키는 벡터입니다.

면을 앞 뒤를 구분하는 이유는 바로 연산량과 관계 있습니다. 구와 같은
물체를 안쪽면까지 그린다면 엄청 느려지겠지요.. 그래서 뒷쪽 면은
연산에서 제외 시켜 버립니다. 이것은 Cull_face 혹은 Cull_mode라고 하져..

또한 노말 벡터는 라이트와 밀접한 관련이 있습니다. 무슨 말인가 하면
당구를 생각합시다. 공을 한쪽 벽에 튀길때 들어 오는 각하고 나오는 각하고
같습니다.



(그림) 법선n
|
i | .o
. | .
. | .
-------------------------당구벽

당구공이 벽과 부딪혀서 들어갈때 각은 법선과 당구공 방향 벡터와 내적으로
구할수 있습니다. 결국 이 내적의 두배의 각도로 튕켜져 나옵니다.

들어가는 공의 방향 벡터가 i라고 하고
법선을 n, 튕겨져 나올 방향 벡터가 o라고 했을때

o = 2(i*n)n - i; 란 벡터 공식이 나옵니다.
(이것도 어떻게 나왔나고 뭍지 마세요..그냥 책에 있음)

식이 좀 어렵죠..

c/c++로 풀이하면..

// 여기서 쓰인 벡터는 노말 벡터로 미리 만들어 줘야 합니다.

// 일단 i 벡터의 방향을 뒤 집고..
i_x = -i_x;
i_y = -i_y;

rad = 2 * ( n_x * i_x + n_y * i_y );
o_x = rad * n_x - i_x;
o_y = rad * n_y - i_y;

이것도 신기하져.. 이걸 잘 응용하면 당구 겜도 만들어요..

(공하고 부딪힐때는 공끼리 부짖히는 점을 법선 벡터로 두면..)

잠시 삼천포로 빠졌군요..
결국 빛을 표면에 뿌릴때 반사되는 각도를 계산하기 위해서 필요한거져..


위의 예제는 2개의 벡터를 가지고 예기 했지만 3D 그래픽에서는
버텍스가 3개입니다. 그럼.. 점 하나를 기준으로 (0,0,0)으로
이동 시켜 버려면 2개만 가지고 위의 외적으로 노말을 구할 수 있습니다.

다음은 노말벡터을 구하는 예제입니다.
물론 연산에 들어가지전 단위 벡터로 만드는 건 잊지 마세요

v1[0] = v0[0] - v1[0];
v1[1] = v0[1] - v1[1];
v1[2] = v0[2] - v1[2];
v2[0] = v1[0] - v2[0];
v2[1] = v1[1] - v2[1];
v2[2] = v1[2] - v2[2];

result[0] = v1[1] * v2[2] - v1[2] * v2[1];
result[1] = v1[2] * v2[0] - v1[0] * v2[2];
result[2] = v1[0] * v2[1] - v1[1] * v2[0];

대충 아시겟져...
이것만 알면 3D 그래픽에 꼭 핵심적인 벡터 연산은 아신겁니다.


추신: 벡터는 참 신기하져? 도강이라도 하세요..

iMusicSoft 주임 연구원 이태경 ( 저 대전 살아요..)

------------------------------------------------------------------------------
벡터 내적강좌중 . 틀린부분이 있습니다..
바로 각도를 구하는부분에서 틀리셔떠군ㅇ -_- 빨리 쓰시느라 그러션나..
theta=a_x*b_x+a_y*b_y; 여기부분부터 미테까지
쭈욱 툴려씁니다...
그게 아니라 그부분엔 costheta=a_x*b_x+a_y*b_y; 라고 구한다음
theta=acos(costheta); 한다음 theta=theta/3.141592654*180; 해야지
제대로된 각이나오죠.. -_-; 백터내적을 구하면 코사인(세타) 값이나오는데
이걸 그냥 cos(코사인(세타)) 로하신격이된네여...
소스로 다시쓰자면요

float a_x, a_y, b_x, b_y;
float v, costheta, theta;

a_x = 1.0; a_y = 3.0;
b_x = 3.0; b_y = 1.0;
// 먼저 단위 벡터로 만든다.
v = sqrt(a_x * a_x + a_y * a_y);
a_x /= v;
a_y /= v;
v = sqrt(b_x * b_x + b_y * b_y);
b_x /= v;

b_y /= v;
costheta = a_x*b_x + a_y*b_y ;
theta = acos(costheta);
theta = theta/3.141592654*180;
가 맞는 소스 입니다

※이글은 하이텔 게임개발자 동호회(gma)의 이태경(수퍼유저)님의 글입니다.

게임은 이벤트처리방식으로 만들지 않는것이 일반적이라서 사용자의 입력을 직접 처리해 주어야 하죠. 특히나 버튼일 경우는 현재 키의 '눌림상태' 만으로 사용자가 한 행동 -아무일도 안하고 있는가,눌렀는가,떼었는가,누른채로 있는가- 을 알아내야 합니다.

이것에 대해서는 여러가지로 설명할수 있지만 제가 생각하기에 가장 이해하기 쉬운것은 진리표에의한 설명인거 같네요.

우선 사용자의 행동을 알아내기위해선 두가지 준비물이 필요합니다.
첫째, 이전 프레임의 키의 '눌림상태'
둘째, 현재 프레임의 키의 '눌림상태'

이것을 간단히 코딩해 본다면 다음처럼 되겠죠...

void main()
{

bool old_key;
bool cur_key;
while(1)
{
cur_key = get_current_keystate();
// A
old_key = cur_key;
}

}

자...이제 'A'라는 부분에 이르면 우리가 원하는 준비물이 각각 old_key와 cur_key에 들어가 있게되겠죠?
자 그럼, 사용자가 무슨짓(?)을 하고있는지 맞혀보도록 합시다.

old_key
cur_key
Event
key state
user action
0
0
None
not pushed
아무일도 않하고 있다
0
1
KeyDown
pushed
키를 눌렀다
1
0
KeyUp
not pushed
키를 떼었다
1
1
None
pushed
누른채로 가만히 있다

이제 상태를 알았으니 'A'라는 부분에 프로그램만 짜넣으면 됩니다.
가령 버튼을 누를때만 총알이 나가야 한다면...
if( !old_key && cur_key ) { 총알발사; }

표를 잘보시면 원리가 눈에 보이죠?

※이글은 김성수님의 글입니다.

 

게임은 일반 윈도우 어플리케이션과는 달리 별다른 메세지가 없어도 자기할일을 묵묵히(?)수행해야 하는 어플리케이션입니다. 덕분에 일반 어플리케이션과는 메세지 루프가 약간 다르죠.

1. 일반적인 메세지 루프

// 메세지가 올때까지 기다린다.
// 메세지가 생기면 메세지큐에서 메세지를 가져온다.
while(GetMessage(&msg....))
{
TranslateMessage(&msg,0,...);
DIspatchMessage(&msg,0...);
}

GetMessage는 메세지가 들어올때까지 블로킹상태로 진입하는 함수입니다.
메세지가 없을때 CPU를 다른 어플리케이션이 사용할수 있도록 해주는 목적이죠.

2. 일반적인 게임용 메세지 루프

while( WM_QUIT != msg.message )
{
// 메세지큐에 메세지가 있는지 검사한다.
// 메세지가 있건,없건 그냥 리턴한다.
if(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
{
//메세지 큐에서 메세지를 가져온다.
GetMessage(&msg,NULL,0,0);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
//메세지가 없다면 게임루프를 돌린다.
GameLoop();
}
}

PeekMessage는 메세지큐에 메세지가 들어와 있는지만을 검사하고 바로 리턴하는 함수 입니다. 맨 마지막에 붙어있는 PM_NOREMOVE 옵션은 메세지가 있는지 검사만 하고 빠져 나오도록 하기위함 입니다.
PeekMessage는 블로킹상태로 들어가는 함수가 아니므로 이런식으로 프로그램을 하면 while루프를 돌리기 위해 CPU를 계속해서 사용하게 됩니다. 하지만 게임은 어차피 계속해서 어떤일을 해야 하는 어플리케이션이니 별로 상관은 없겠죠.
다만 위 예제는 게임이 비활성화 되었을때의 처리가 없습니다.

3. DX8 예제나오는 메세지 루프

while( WM_QUIT != msg.message )
{
if( m_bActive ) bGotMsg = PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE );
else bGotMsg = GetMessage( &msg, NULL, 0U, 0U );

if( bGotMsg )
{
// Translate and dispatch the message
if( 0 == TranslateAccelerator( m_hWnd, hAccel, &msg ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}
else
{
// Render a frame during idle time (no messages are waiting)
if( m_bActive && m_bReady )
{
if( FAILED( Render3DEnvironment() ) )
SendMessage( m_hWnd, WM_CLOSE, 0, 0 );
}
}
}

PeekMessage엣 PM_REMOVE옵션을 쓴것에 주의하세요.
이렇게 하면 메세지가 있을경우엔 GetMessage와 동일한 동작을 합니다. 메세지가 없다면 PM_NOREMOVE옵션을 쓴거랑 똑같이 동작하죠.

m_bActive는 윈도우가 비활성화되었을때 게임이 돌지 않도록 하는겁니다.
윈도우가 비활성화 되었는데도 PeekMessage를 계속사용하는건 별로 바람직하지 못하기 때문에...( GetMessage를 쓰면 블로킹상태로 들어가는데 PeekMessage를 쓰면 계속해서 while 루프가 돌게되죠 ) ...

4. 함수형 메세지 루프

마지막으로 편법이지만 도스처럼 프램할수도 있습니다.
다음과 같은 일을하는 함수를 만들어서 될수 있는한 자주 호출해 주는거죠..

bool _ProcessMessage()
{
MSG msg;
bool bGotMsg;
while( 1 )
{
if( m_bActive ) bGotMsg = PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE );
else bGotMsg = GetMessage( &msg, NULL, 0U, 0U );

if( !bGotMsg ) return false;

TranslateMessage( &msg );
DispatchMessage( &msg );
if( WM_QUIT == msg.message ) return true; //exit...
}
return false;
}

이렇게 해놓구 게임에서 암때나 마구잡이로 호출하면 되죠. ...

WinMain(...)
{
...
while(1)
{
if( _ProcessMessage() )break;
//게임내용
...
}
//종료
...
}

이 방법의 장점은 도스처럼 마구잡이(?)프램이 가능하다는 것입니다.
프로그램 어디서건 _ProcessMessage()만 호출해주면 메세지처리는 되니깐 윈도우 메세지때문에 무한루프를 못돌리는 일은 없어집니다.

제가 생각해서 썼던거지만, 다른분들두 이렇게 하신분들이 분명 있을테고, 어떤 Mpeg2Player 소스를 보니 거기서도 이 방법을 썼더군요.

하지만.....

이 마지막 방법은 별루 권하구 싶지는 않은 방법이네요.
도스프램만 하시던 분이라면 몰라도, 첨 배우시는 분들은 제대로된 메세지 루프를 가지는 프램으로 만드시길...